[
  {
    "path": ".dockerignore",
    "content": "vendor\nui/node_modules\npb_data\nbuild\n.vscode\n"
  },
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = crlf\nindent_size = 2\nindent_style = space\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.go]\ncharset = utf-8\nend_of_line = lf\nindent_size = 2\nindent_style = tab\n"
  },
  {
    "path": ".gitattributes",
    "content": "﻿* text=auto eol=crlf\n*.go text eol=lf\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\npolar: # Replace with a single Polar username\nbuy_me_a_coffee: # Replace with a single Buy Me a Coffee username\nthanks_dev: # Replace with a single thanks.dev username\ncustom: [\"https://profile.ikit.fun/sponsors/\"]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/1-bug_report.yml",
    "content": "name: \"🐞 Bug Report\"\ndescription: \"Create a report to help us improve. / 报告缺陷来帮助我们完善。\"\ntitle: \"[Bug] Describe the Bug briefly / 简要描述你发现的缺陷\"\ntype: bug\nlabels:\n  - bug\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        **Before you submit the issue, please make sure of the following checklist**:\n        1. Yes, I'm using the latest release and can reproduce the issue. Issues that are not in the latest version will be closed directly.\n        2. Yes, I've searched for [existing issues](https://github.com/certimate-go/certimate/issues) (including closed ones) on GitHub and didn't find any similar.\n        3. Yes, I've read the [documentation](https://docs.certimate.me/) and didn't find any similar.\n        4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly.\n        5. Please limit one report per issue.\n\n        **在提交 Issue 之前，请确认以下事项**：\n        1. 我**确认**已尝试过使用当前最新版本，并能复现问题。由于开发者精力有限，非当前最新版本的问题将被直接关闭，感谢理解。\n        2. 我**确认**已搜索过[已有的 Issues](https://github.com/certimate-go/certimate/issues)（包括已关闭的），没有类似的问题。\n        3. 我**确认**已阅读过[文档](https://docs.certimate.me/)，没有类似的问题。\n        4. 请**务必**按照模板规范详细描述问题，否则 Issue 将会被直接关闭。\n        5. 请保持每个 Issue 只包含一个缺陷报告。如果有多个缺陷，请分别提交 Issue。\n\n  - type: input\n    attributes:\n      label: Release Version / 软件版本\n      description: Please provide the specific version of Certimate. / 请提供 Certimate 的具体版本（请不要填写 `latest` 之类的无效版本号）。\n      placeholder: (e.g. v1.0.0. `latest` is **NOT** a valid version!)\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Description / 缺陷描述\n      description: Describe the bug you found in detail and clearly, and upload screenshots if possible. / 请详细清晰地描述你发现的缺陷或故障，如果可能请上传截图。\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Steps to reproduce / 复现步骤\n      description: Please walk us through it step by step. / 请提供可复现的完整步骤。\n      placeholder: |\n        1. ...\n        2. ...\n        3. ...\n        ...\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Logs / 日志\n      description: Add logs here if available. / 在此处添加日志信息（如果有的话）。\n      value: |-\n        <details>\n\n        ```console\n        # Paste logs here / 请在此粘贴日志\n        ```\n\n        </details>\n    validations:\n      required: false\n\n  - type: textarea\n    attributes:\n      label: Miscellaneous / 其他\n      description: Add any other context about the issue here. / 在此处添加关于该 Issue 的任何其他信息。\n    validations:\n      required: false\n\n  - type: checkboxes\n    attributes:\n      label: Contribution / 贡献代码\n      options:\n        - label: I am interested in contributing a PR for this! / 我乐意为此提交代码并发起 PR！\n          required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/2-feature_request.yml",
    "content": "name: \"💡 Feature Request\"\ndescription: \"Suggest an idea for this project. / 提出新功能请求或改进意见。\"\ntitle: \"[Feature] Describe the feature briefly / 简要描述你希望实现的功能\"\ntype: feature\nlabels:\n  - enhancement\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        **Before you submit the issue, please make sure of the following checklist**:\n        1. Yes, I'm using the latest release.\n        2. Yes, I've searched for [existing issues](https://github.com/certimate-go/certimate/issues) (including closed ones) on GitHub and didn't find any similar.\n        3. Yes, I've read the [documentation](https://docs.certimate.me/) and didn't find any similar.\n        4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly.\n        5. Please limit one request per issue.\n\n        **在提交 Issue 之前，请确认以下事项**：\n        1. 我**确认**是基于当前最新大版本而提出的新功能请求或改进意见。\n        2. 我**确认**已搜索过[已有的 Issues](https://github.com/certimate-go/certimate/issues)（包括已关闭的），没有类似的问题。\n        3. 我**确认**已阅读过[文档](https://docs.certimate.me/)，没有类似的问题。\n        4. 请**务必**按照模板规范详细描述问题，否则 Issue 将会被直接关闭。\n        5. 请保持每个 Issue 只包含一个功能请求。如果有多个需求，请分别提交 Issue。\n\n  - type: textarea\n    attributes:\n      label: Description / 功能描述\n      description: Describe the feature you'd like to add in detail and clearly, and upload screenshots if possible. / 请详细清晰地描述你希望添加的功能，如果可能请上传截图。\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Motivation / 请求动机\n      description: Why is this feature helpful to the project? / 为什么这个功能对项目有帮助？\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Miscellaneous / 其他\n      description: Add any other context about the problem here. / 在此处添加关于该 Issue 的任何其他信息（新增提供商请求请补充 API 文档链接等资料）。\n    validations:\n      required: false\n\n  - type: checkboxes\n    attributes:\n      label: Contribution / 贡献代码\n      options:\n        - label: I am interested in contributing a PR for this! / 我乐意为此提交代码并发起 PR！\n          required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/3-questions.yml",
    "content": "name: \"❓ Questions\"\ndescription: \"Have problem in use and need help? / 遇到了困难需要求助？\"\ntitle: \"Describe the question briefly / 简要描述你遇到的问题\"\ntype: question\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        **Before you submit the issue, please make sure of the following checklist**:\n        1. Yes, I'm using the latest release.\n        2. Yes, I've searched for [existing issues](https://github.com/certimate-go/certimate/issues) (including closed ones) on GitHub and didn't find any similar.\n        3. Yes, I've read the [documentation](https://docs.certimate.me/) and didn't find any similar.\n        4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly.\n        5. Please limit one question per issue.\n\n        **在提交 Issue 之前，请确认以下事项**：\n        1. 我**确认**正在使用的是当前最新版本。\n        2. 我**确认**已搜索过[已有的 Issues](https://github.com/certimate-go/certimate/issues)（包括已关闭的），没有类似的问题。\n        3. 我**确认**已阅读过[文档](https://docs.certimate.me/)，没有类似的问题。\n        4. 请**务必**按照模板规范详细描述问题，否则 Issue 将会被直接关闭。\n        5. 请保持每个 Issue 只包含一个问题求助。如果有多个问题，请分别提交 Issue。\n\n  - type: input\n    attributes:\n      label: Release Version / 软件版本\n      description: Please provide the specific version of Certimate. / 请提供 Certimate 的具体版本（请不要填写 `latest` 之类的无效版本号）。\n      placeholder: (e.g. v1.0.0)\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Description / 问题描述\n      description: Describe the problem you encountered in detail and clearly, and upload screenshots if possible. / 请详细清晰地描述你遇到的问题，如果可能请上传截图。\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Miscellaneous / 其他\n      description: Add any other context about the problem here. / 在此处添加关于该问题的任何其他信息。\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: \"🌐 Community / 社群讨论\"\n    about: \"Join in our Telegram channel. / 加入到电报频道寻求更多帮助。\"\n    url: \"https://t.me/+ZXphsppxUg41YmVl\"\n  - name: \"📖 FAQ / 常见问题\"\n    about: \"Please take a look to FAQs. / 请先阅读文档 FAQ，可能会有你需要的答案。\"\n    url: \"https://docs.certimate.me/docs/reference/faq\"\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "﻿<!--\n\nIMPORTANT! Before opening a Pull Request:\n  - Please ensure you have read the `CONTRIBUTING.md` in the repository.\n  - Please ensure you have opened an issue first, and got feedback from the maintainers.\n  - Please ensure you are **NOT** fork within a GitHub organization, to [allowing edits by maintainers](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork).\n  - By submitting this pull request, you agree to license your contributions under the terms of the MIT License.\n\n---\n\n重要必读！在提交 PR 之前：\n  - 请确保已阅读仓库根目录下的 `CONTRIBUTING_zh.md` 贡献指南。\n  - 请确保已创建了一个关联的 Issue，并等待来自维护者的反馈。\n  - 请确保使用的是个人 fork、而非组织 fork 来创建 PR，以便维护者可以[编辑拉取请求](https://docs.github.com/zh/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork)。\n  - 提交本 PR 即表示，您同意可依据 MIT 许可协议的相关条款对您的贡献进行授权。\n\n-->\n"
  },
  {
    "path": ".github/workflows/push_image.yml",
    "content": "name: Docker Image CI (stable versions)\n\non:\n  push:\n    tags:\n      - \"v[0-9]*\"\n      - \"!v*alpha*\"\n      - \"!v*beta*\"\n      - \"!v*rc*\"\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: \"Tag version to be used for Docker image\"\n        required: true\n        default: \"latest\"\n\njobs:\n  prepare-ui:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n\n      - name: Build UI\n        run: |\n          npm --prefix=./ui ci\n          npm --prefix=./ui run build\n\n      - name: Upload UI build artifacts\n        uses: actions/upload-artifact@v5\n        with:\n          name: ui-build\n          path: ./ui/dist\n          retention-days: 1\n\n  build-and-push:\n    needs: prepare-ui\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Free disk space\n        uses: BRAINSia/free-disk-space@v2\n        with:\n          tool-cache: false\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            certimate/certimate\n            registry.cn-shanghai.aliyuncs.com/certimate/certimate\n          tags: |\n            type=ref,event=branch\n            type=ref,event=pr\n            type=semver,pattern=v{{version}}\n            type=semver,pattern=v{{major}}.{{minor}}\n\n      - name: Log in to DOCKERHUB\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_PASSWORD }}\n\n      - name: Log in to ALIYUNCS\n        uses: docker/login-action@v3\n        with:\n          registry: registry.cn-shanghai.aliyuncs.com\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKER_PASSWORD }}\n\n      - name: Download UI build artifacts\n        uses: actions/download-artifact@v6\n        with:\n          name: ui-build\n          path: ./ui/dist\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          # file: ./Dockerfile\n          file: ./Dockerfile.gh\n          platforms: linux/amd64,linux/arm64,linux/arm/v7\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n"
  },
  {
    "path": ".github/workflows/push_image_next.yml",
    "content": "name: Docker Image CI (preview versions)\n\non:\n  push:\n    tags:\n      - \"v[0-9]*-alpha*\"\n      - \"v[0-9]*-beta*\"\n      - \"v[0-9]*-rc*\"\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: \"Tag version to be used for Docker image\"\n        required: true\n        default: \"next\"\n\njobs:\n  prepare-ui:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n\n      - name: Build UI\n        run: |\n          npm --prefix=./ui ci\n          npm --prefix=./ui run build\n\n      - name: Upload UI build artifacts\n        uses: actions/upload-artifact@v5\n        with:\n          name: ui-build\n          path: ./ui/dist\n          retention-days: 1\n\n  build-and-push:\n    needs: prepare-ui\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Free disk space\n        uses: BRAINSia/free-disk-space@v2\n        with:\n          tool-cache: false\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            certimate/certimate\n            registry.cn-shanghai.aliyuncs.com/certimate/certimate\n          tags: |\n            type=ref,event=tag,pattern={{version}}\n            type=raw,value=next\n          flavor: |\n            latest=false\n\n      - name: Log in to DOCKERHUB\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_PASSWORD }}\n\n      - name: Log in to ALIYUNCS\n        uses: docker/login-action@v3\n        with:\n          registry: registry.cn-shanghai.aliyuncs.com\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKER_PASSWORD }}\n\n      - name: Download UI build artifacts\n        uses: actions/download-artifact@v6\n        with:\n          name: ui-build\n          path: ./ui/dist\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          # file: ./Dockerfile\n          file: ./Dockerfile.gh\n          platforms: linux/amd64,linux/arm64,linux/arm/v7\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - \"v[0-9]*\"\n\njobs:\n  prepare-ui:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n\n      - name: Build UI\n        run: |\n          npm --prefix=./ui ci\n          npm --prefix=./ui run build\n\n      - name: Upload UI build artifacts\n        uses: actions/upload-artifact@v5\n        with:\n          name: ui-build\n          path: ./ui/dist\n          retention-days: 1\n\n  build-linux:\n    needs: prepare-ui\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: \"go.mod\"\n\n      - name: Download UI build artifacts\n        uses: actions/download-artifact@v6\n        with:\n          name: ui-build\n          path: ./ui/dist\n\n      - name: Build Linux binaries\n        env:\n          CGO_ENABLED: 0\n          GOOS: linux\n        run: |\n          mkdir -p dist/linux\n          for ARCH in amd64 arm64 armv7; do\n            if [ \"$ARCH\" == \"armv7\" ]; then\n              go env -w GOARCH=arm\n              go env -w GOARM=7\n            else\n              go env -w GOARCH=$ARCH\n              go env -u GOARM\n            fi\n            go build -trimpath -ldflags=\"-s -w\" -o dist/linux/certimate_${GITHUB_REF#refs/tags/}_linux_$ARCH\n          done\n\n      - name: Upload Linux binaries\n        uses: actions/upload-artifact@v5\n        with:\n          name: linux-binaries\n          path: dist/linux/\n          retention-days: 1\n\n  build-macos:\n    needs: prepare-ui\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: \"go.mod\"\n\n      - name: Download UI build artifacts\n        uses: actions/download-artifact@v6\n        with:\n          name: ui-build\n          path: ./ui/dist\n\n      - name: Build macOS binaries\n        env:\n          CGO_ENABLED: 0\n          GOOS: darwin\n        run: |\n          mkdir -p dist/darwin\n          for ARCH in amd64 arm64; do\n            go env -w GOARCH=$ARCH\n            go build -trimpath -ldflags=\"-s -w\" -o dist/darwin/certimate_${GITHUB_REF#refs/tags/}_darwin_$ARCH\n          done\n\n      - name: Upload macOS binaries\n        uses: actions/upload-artifact@v5\n        with:\n          name: macos-binaries\n          path: dist/darwin/\n          retention-days: 1\n\n  build-windows:\n    needs: prepare-ui\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: \"go.mod\"\n\n      - name: Download UI build artifacts\n        uses: actions/download-artifact@v6\n        with:\n          name: ui-build\n          path: ./ui/dist\n\n      - name: Build Windows binaries\n        env:\n          CGO_ENABLED: 0\n          GOOS: windows\n        run: |\n          mkdir -p dist/windows\n          for ARCH in amd64 arm64 386; do\n            go env -w GOARCH=$ARCH\n            go build -trimpath -ldflags=\"-s -w\" -o dist/windows/certimate_${GITHUB_REF#refs/tags/}_windows_$ARCH.exe\n          done\n\n      - name: Upload Windows binaries\n        uses: actions/upload-artifact@v5\n        with:\n          name: windows-binaries\n          path: dist/windows/\n          retention-days: 1\n\n  create-release:\n    needs: [build-linux, build-macos, build-windows]\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Download all binaries\n        uses: actions/download-artifact@v6\n        with:\n          path: ./artifacts\n\n      - name: Prepare release assets\n        run: |\n          mkdir -p dist\n          cp -r artifacts/linux-binaries/* dist/\n          cp -r artifacts/macos-binaries/* dist/\n          cp -r artifacts/windows-binaries/* dist/\n\n          find dist -type f -not -name \"*.exe\" -exec chmod +x {} \\;\n\n          cd dist\n          for bin in certimate_*; do\n            if [[ \"$bin\" == *\".exe\" ]]; then\n              entrypoint=\"certimate.exe\"\n            else\n              entrypoint=\"certimate\"\n            fi\n\n            tmpdir=$(mktemp -d)\n            cp \"$bin\" \"${tmpdir}/${entrypoint}\"\n            cp ../LICENSE \"$tmpdir/LICENSE\"\n            cp ../README.md \"$tmpdir/README.md\"\n            cp ../CHANGELOG.md \"$tmpdir/CHANGELOG.md\"\n\n            if [[ \"$bin\" == *\".exe\" ]]; then\n              zip -j \"${bin%.exe}.zip\" \"$tmpdir\"/*\n            else\n              zip -j -X \"${bin}.zip\" \"$tmpdir\"/*\n            fi\n\n            rm -rf \"$tmpdir\"\n          done\n\n          sha256sum *.zip > checksums.txt\n\n      - name: Create Release\n        uses: softprops/action-gh-release@v2\n        with:\n          files: |\n            dist/*.zip\n            dist/checksums.txt\n          draft: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/release_sync_gitee.py",
    "content": "﻿#!/usr/bin/env python3\nimport logging\nimport json\nimport mimetypes\nimport tempfile\nimport os\nimport random\nimport re\nimport shutil\nimport time\nfrom urllib import request\nfrom urllib.error import HTTPError\n\nGITHUB_REPO = \"certimate-go/certimate\"\nGITEE_REPO = \"certimate-go/certimate\"\nGITEE_TOKEN = os.getenv(\"GITEE_TOKEN\", \"\")\n\nSYNC_MARKER = \"SYNCING FROM GITHUB, PLEASE WAIT ...\"\nTEMP_DIR = tempfile.mkdtemp()\n\nlogging.basicConfig(level=logging.INFO)\n\n\ndef do_httpreq(url, method=\"GET\", headers=None, data=None):\n    req = request.Request(url, data=data, method=method)\n    headers = headers or {}\n    for key, value in headers.items():\n        req.add_header(key, value)\n\n    try:\n        with request.urlopen(req) as resp:\n            resp_data = resp.read().decode(\"utf-8\")\n            if resp_data:\n                try:\n                    return json.loads(resp_data)\n                except json.JSONDecodeError:\n                    pass\n            return None\n    except HTTPError as e:\n        errmsg = \"\"\n        if e.readable():\n            try:\n                errmsg = e.read().decode('utf-8')\n                errmsg = errmsg.replace(\"\\r\", \"\\\\r\").replace(\"\\n\", \"\\\\n\")\n            except:\n                pass\n        logging.error(f\"Error occurred when sending request: status={e.status}, response={errmsg}\")\n        raise e\n    except Exception as e:\n        raise e\n\n\ndef get_github_stable_release():\n    page = 1\n    while True:\n        releases = do_httpreq(\n            url=f\"https://api.github.com/repos/{GITHUB_REPO}/releases?page={page}&per_page=100\",\n            headers={\"Accept\": \"application/vnd.github+json\"},\n        )\n        if not releases or len(releases) == 0:\n            break\n\n        for release in releases:\n            release_name = release.get(\"name\", \"\")\n            if re.match(r\"^v[0-9]\", release_name):\n                if any(\n                    x in release_name\n                    for x in [\"alpha\", \"beta\", \"rc\", \"preview\", \"test\", \"unstable\"]\n                ):\n                    continue\n                return release\n\n        page += 1\n\n    return None\n\n\ndef get_gitee_release_list():\n    page = 1\n    list = []\n    while True:\n        releases = do_httpreq(\n            url=f\"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases?access_token={GITEE_TOKEN}&page={page}&per_page=100\",\n        )\n        if not releases or len(releases) == 0:\n            break\n\n        list.extend(releases)\n        page += 1\n\n    return list\n\n\ndef get_gitee_release_by_tag(tag_name):\n    releases = get_gitee_release_list()\n    for release in releases:\n        if release.get(\"tag_name\") == tag_name:\n            return release\n\n    return None\n\n\ndef delete_gitee_release(release_info):\n    if not release_info:\n        raise ValueError(\"Release info is invalid\")\n\n    release_id = release_info.get(\"id\", \"\")\n    release_name = release_info.get(\"tag_name\", \"\")\n    if not release_id:\n        raise ValueError(\"Release ID is missing\")\n\n    attachpage = 1\n    attachfiles = []\n    while True:\n        releases = do_httpreq(\n            url=f\"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}/attach_files?access_token={GITEE_TOKEN}&page={attachpage}&per_page=100\",\n        )\n        if not releases or len(releases) == 0:\n            break\n\n        attachfiles.extend(releases)\n        attachpage += 1\n\n    for attachfile in attachfiles:\n        attachfile_id = attachfile.get(\"id\")\n        attachfile_name = attachfile.get(\"name\")\n        logging.info(\"Trying to delete Gitee attach file: %s/%s\", release_name, attachfile_name)\n        do_httpreq(\n            url=f\"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}/attach_files/{attachfile_id}?access_token={GITEE_TOKEN}\",\n            method=\"DELETE\",\n        )\n\n    logging.info(\"Trying to delete Gitee release: %s\", release_name)\n    do_httpreq(\n        url=f\"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}?access_token={GITEE_TOKEN}\",\n        method=\"DELETE\",\n    )\n\n\ndef create_gitee_release(name, tag, body, prerelease, gh_assets):\n    release_info = do_httpreq(\n        f\"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases?access_token={GITEE_TOKEN}\",\n        method=\"POST\",\n        headers={\"Content-Type\": \"application/json\"},\n        data=json.dumps({\n            \"tag_name\": tag,\n            \"name\": name,\n            \"body\": SYNC_MARKER,\n            \"prerelease\": prerelease,\n            \"target_commitish\": \"\",\n        }).encode(\"utf-8\"),\n    )\n\n    if not release_info or \"id\" not in release_info:\n        return None\n    logging.info(\"Gitee release created\")\n\n    release_id = release_info[\"id\"]\n\n    assets_dir = os.path.join(TEMP_DIR, \"assets\")\n    os.makedirs(assets_dir, exist_ok=True)\n\n    gh_assets = gh_assets or []\n    for asset in gh_assets:\n        logging.info(\"Tring to download asset from GitHub: %s\", asset[\"name\"])\n\n        opener = request.build_opener()\n        request.install_opener(opener)\n        download_ts = time.time()\n        download_url = asset.get(\"browser_download_url\")\n        download_path = os.path.join(assets_dir, asset[\"name\"])\n        def _hook(blocknum, blocksize, totalsize):\n            nonlocal download_ts\n            TIMESPAN = 5 # print progress every 5sec\n            ts = time.time()\n            pct = min(round(100 * blocknum * blocksize / totalsize, 2), 100)\n            if (ts - download_ts < TIMESPAN) and (pct < 100):\n                return\n            download_ts = ts\n            logging.info(f\"Downloading {download_url} >>> {pct}%\")\n\n        request.urlretrieve(download_url, download_path, _hook)\n\n    for asset in gh_assets:\n        logging.info(\"Tring to upload asset to Gitee: %s\", asset[\"name\"])\n\n        boundary = '----boundary' + ''.join(random.choice('0123456789abcdef') for _ in range(16))\n        print(f\"Using boundary: {boundary}\")\n        with open(os.path.join(assets_dir, asset[\"name\"]), 'rb') as f:\n            attachfile_mime = mimetypes.guess_type(asset[\"name\"])[0] or 'application/octet-stream'\n            attachfile_req = []\n            attachfile_req.append(f\"--{boundary}\")\n            attachfile_req.append(f'Content-Disposition: form-data; name=\"file\"; filename=\"{asset[\"name\"]}\"')\n            attachfile_req.append(f\"Content-Type: {attachfile_mime}\")\n            attachfile_req.append(\"\")\n            attachfile_req.append(f.read().decode('latin-1'))\n            attachfile_req.append(f\"--{boundary}--\")\n            attachfile_req.append(\"\")\n            attachfile_req = \"\\r\\n\".join(attachfile_req).encode('latin-1')\n\n            do_httpreq(\n                f\"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}/attach_files?access_token={GITEE_TOKEN}\",\n                method=\"POST\",\n                headers={'Content-Type': f'multipart/form-data; boundary={boundary}'},\n                data=attachfile_req,\n            )\n            logging.info(\"Asset uploaded: %s\", asset[\"name\"])\n\n    release_info = do_httpreq(\n        f\"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}?access_token={GITEE_TOKEN}\",\n        method=\"PATCH\",\n        headers={\"Content-Type\": \"application/json\"},\n        data=json.dumps({\n            \"tag_name\": tag,\n            \"name\": name,\n            \"body\": f\"**此发行版同步自 GitHub，完整变更日志请访问 https://github.com/{GITHUB_REPO}/releases/{tag} 查看。**\\n\\n**因 Gitee 存储空间容量有限，仅能保留最新一个发行版，如需其余版本请访问 GitHub 获取。**\\n\\n---\\n\\n\" + body,\n            \"prerelease\": prerelease,\n        }).encode(\"utf-8\"),\n    )\n    logging.info(\"Gitee release updated\")\n    return release_info\n\n\ndef main():\n    try:\n        # 获取 GitHub 最新稳定发行版\n        github_release = get_github_stable_release()\n        if not github_release:\n            logging.warning(\"GitHub stable release not found. Foget to release?\")\n            return\n        else:\n            logging.info(\"GitHub stable release found: %s\", github_release.get('name'))\n\n        # 提取稳定版的信息\n        release_name = github_release.get(\"name\")\n        release_tag = github_release.get(\"tag_name\")\n        release_body = github_release.get(\"body\")\n        release_prerelease = github_release.get(\"prerelease\", False)\n        release_assets = github_release.get(\"assets\", [])\n\n        # 检查 Gitee 是否已有同名发行版\n        gitee_release = get_gitee_release_by_tag(release_tag)\n        if gitee_release and gitee_release.get(\"body\") == SYNC_MARKER:\n            logging.warning(\"Gitee syncing release found, cleaning up...\")\n            delete_gitee_release(gitee_release)\n        elif gitee_release:\n            logging.info(\"Gitee release already exists, exit.\")\n            return\n\n        # 同步发行版\n        gitee_release = create_gitee_release(release_name, release_tag, release_body, release_prerelease, release_assets)\n        if not gitee_release:\n            logging.warning(\"Failed to create Gitee release.\")\n            return\n\n        # 清除历史发行版\n        gitee_release_list = get_gitee_release_list()\n        for release in gitee_release_list:\n            if release.get(\"tag_name\") == release_tag:\n                continue\n            else:\n                delete_gitee_release(release)\n\n        logging.info(\"Sync release completed.\")\n\n    except Exception as e:\n        logging.fatal(str(e))\n        exit(1)\n\n    finally:\n        if os.path.exists(TEMP_DIR):\n            shutil.rmtree(TEMP_DIR)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": ".github/workflows/release_sync_gitee.yml",
    "content": "name: Release Sync to Gitee\n\non:\n  # release:\n  #   types: [published, unpublished, deleted]\n  workflow_dispatch:\n\njobs:\n  sync-to-gitee:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Set up Python3\n        uses: actions/setup-python@v6\n        with:\n          python-version: \"3.13\"\n\n      - name: Run script\n        env:\n          GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}\n        run: |\n          cd .github/workflows\n          python ./release_sync_gitee.py\n"
  },
  {
    "path": ".gitignore",
    "content": ".vscode/*\n!.vscode/extensions.json\n!.vscode/settings.json\n!.vscode/settings.tailwind.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n__debug_bin*\n\nvendor\npb_data\nbuild\nmain\n/dist\n/docker/data\n/certimate\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "project_name: certimate\n\ndist: .builds\n\nbefore:\n  hooks:\n    - go mod tidy\n\nbuilds:\n  - id: build_noncgo\n    main: ./\n    binary: certimate\n    env:\n      - CGO_ENABLED=0\n    flags:\n      - -trimpath\n    ldflags:\n      - -s -w -X github.com/certimate-go/certimate.Version={{ .Version }}\n    goos:\n      - linux\n      - windows\n      - darwin\n    goarch:\n      - amd64\n      - arm64\n      - arm\n    goarm:\n      - 7\n    ignore:\n      - goos: windows\n        goarch: arm\n      - goos: darwin\n        goarch: arm\n\n# upx:\n#   - enabled: true\n\nrelease:\n  draft: true\n\narchives:\n  - id: archive_noncgo\n    builds: [build_noncgo]\n    format: \"zip\"\n    files:\n      - LICENSE\n      - README.md\n      - CHANGELOG.md\n\nchecksum:\n  name_template: \"checksums.txt\"\n\nsnapshot:\n  name_template: \"{{ incpatch .Version }}-next\"\n\nchangelog:\n  sort: asc\n  filters:\n    exclude:\n      - \"^ui:\"\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"bradlc.vscode-tailwindcss\",\n    \"dbaeumer.vscode-eslint\",\n    \"esbenp.prettier-vscode\",\n  ]\n}"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"css.customData\": [\n    \".vscode/settings.tailwind.json\"\n  ],\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"explicit\"\n  },\n  \"editor.defaultFormatter\": \"dbaeumer.vscode-eslint\",\n  \"editor.formatOnSave\": true,\n  \"go.useLanguageServer\": true,\n  \"gopls\": {\n    \"formatting.gofumpt\": true,\n  },\n  \"typescript.tsdk\": \"ui/node_modules/typescript/lib\",\n  \"[go]\": {\n    \"editor.defaultFormatter\": \"golang.go\"\n  },\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  }\n}"
  },
  {
    "path": ".vscode/settings.tailwind.json",
    "content": "{\n  \"version\": 1.1,\n  \"atDirectives\": [\n    {\n      \"name\": \"@apply\",\n      \"description\": \"Use the `@apply` directive to inline any existing utility classes into your own custom CSS.\",\n      \"references\": [\n        {\n          \"name\": \"Tailwind Documentation\",\n          \"url\": \"https://tailwindcss.com/docs/functions-and-directives#variant-directive\"\n        }\n      ]\n    },\n    {\n      \"name\": \"@source\",\n      \"description\": \"Use the `@source` directive to explicitly specify source files that aren't picked up by Tailwind's automatic content detection.\",\n      \"references\": [\n        {\n          \"name\": \"Tailwind Documentation\",\n          \"url\": \"https://tailwindcss.com/docs/functions-and-directives#source-directive\"\n        }\n      ]\n    },\n    {\n      \"name\": \"@theme\",\n      \"description\": \"Use the `@theme` directive to define your project's custom design tokens, like fonts, colors, and breakpoints.\",\n      \"references\": [\n        {\n          \"name\": \"Tailwind Documentation\",\n          \"url\": \"https://tailwindcss.com/docs/functions-and-directives#theme-directive\"\n        }\n      ]\n    },\n    {\n      \"name\": \"@utility\",\n      \"description\": \"Use the `@utility` directive to add custom utilities to your project that work with variants like `hover`, `focus` and `lg`.\",\n      \"references\": [\n        {\n          \"name\": \"Tailwind Documentation\",\n          \"url\": \"https://tailwindcss.com/docs/functions-and-directives#utility-directive\"\n        }\n      ]\n    },\n    {\n      \"name\": \"@variant\",\n      \"description\": \"Use the `@variant` directive to apply a Tailwind variant to styles in your CSS\",\n      \"references\": [\n        {\n          \"name\": \"Tailwind Documentation\",\n          \"url\": \"https://tailwindcss.com/docs/functions-and-directives#variant-directive\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "A full changelog of past releases is available on [GitHub Releases](https://github.com/certimate-go/certimate/releases) page.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contribution Guide\n\n<div align=\"center\">\n\nEnglish ｜ [简体中文](CONTRIBUTING_zh.md)\n\n</div>\n\nThank you for taking the time to improve Certimate! Below is a guide for submitting a PR (Pull Request) to the Certimate repository.\n\nWe need to be nimble and ship fast given where we are, but we also want to make sure that contributors like you get as smooth an experience at contributing as possible. We've assembled this contribution guide for that purpose, aiming at getting you familiarized with the codebase & how we work with contributors, so you could quickly jump to the fun part.\n\nIndex:\n\n- [Development](#development)\n  - [Prerequisites](#prerequisites)\n  - [Backend Code](#backend-code)\n  - [Frontend Code](#frontend-code)\n- [Submitting PR](#submitting-pr)\n  - [Pull Request Process](#pull-request-process)\n- [Getting Help](#getting-help)\n\n---\n\n## Development\n\n### Prerequisites\n\n- Go 1.25+ (for backend code changes)\n- Node.js 22.12+ (for frontend code changes)\n\n### Backend Code\n\nThe backend code of Certimate is developed using Golang. It is a monolithic application based on [Pocketbase](https://github.com/pocketbase/pocketbase).\n\nOnce you have made changes to the backend code in Certimate, follow these steps to run the project:\n\n1. Navigate to the root directory.\n2. Install dependencies:\n   ```bash\n   go mod vendor\n   ```\n3. Start the local development server:\n   ```bash\n   go run main.go serve\n   ```\n\nThis will start a web server at `http://localhost:8090` using the prebuilt WebUI located in `/ui/dist`.\n\n> If you encounter an error `ui/embed.go: pattern all:dist: no matching files found`, please refer to _[Frontend Code](#frontend-code)_ and build WebUI first.\n\n**Before submitting a PR to the main repository, you should:**\n\n- Format your source code by using [gofumpt](https://github.com/mvdan/gofumpt). Recommended using VSCode and installing the gofumpt plugin to automatically format when saving.\n- Adding unit or integration tests for your changes (with go standard library `testing` package).\n\n### Frontend Code\n\nThe frontend code of Certimate is developed using TypeScript. It is a SPA based on [React](https://github.com/facebook/react) and [Vite](https://github.com/vitejs/vite).\n\nOnce you have made changes to the backend code in Certimate, follow these steps to run the project:\n\n1. Navigate to the `/ui` directory.\n2. Install dependencies:\n   ```bash\n   npm install\n   ```\n3. Start the local development server:\n   ```bash\n   npm run dev\n   ```\n\nThis will start a web server at `http://localhost:5173`. You can now access the WebUI in your browser.\n\nAfter completing your changes, build the WebUI so it can be embedded into the Go package:\n\n```bash\nnpm run build\n```\n\n**Before submitting a PR to the main repository, you should:**\n\n- Format your source code by using [ESLint](https://github.com/eslint/eslint). Recommended using VSCode and installing the ESLint plugin to automatically format when saving.\n\n## Submitting PR\n\nBefore opening a Pull Request, please open an issue to discuss the change and get feedback from the maintainers. This will helps us:\n\n- To understand the context of the change.\n- To ensure it fits into Certimate's roadmap.\n- To prevent us from duplicating work.\n- To prevent you from spending time on a change that we may not be able to accept.\n\n### Pull Request Process\n\n1. Fork the repository, and then checkout `main` branch.\n2. Before you draft a PR, please open an issue to discuss the changes you want to make.\n3. Create a new branch for your changes.\n4. Please add tests for your changes accordingly.\n5. Ensure your code passes the existing tests.\n6. Please link the issue in the PR description.\n7. Get merged!\n\n> [!IMPORTANT]\n>\n> It is recommended to create a new branch from `main` for each bug fix or feature. If you plan to submit multiple PRs, ensure the changes are in separate branches for easier review and eventual merge.\n>\n> Keep each PR focused on a single feature or fix.\n\n## Getting Help\n\nIf you ever get stuck or get a burning question while contributing, simply shoot your queries our way via the GitHub issues.\n"
  },
  {
    "path": "CONTRIBUTING_zh.md",
    "content": "# 贡献指南\n\n<div align=\"center\">\n\n[English](CONTRIBUTING.md) ｜ 简体中文\n\n</div>\n\n非常感谢你抽出时间来帮助改进 Certimate！以下是向 Certimate 提交 Pull Request 时的操作指南。\n\n我们需要保持敏捷和快速迭代，同时也希望确保贡献者能获得尽可能流畅的参与体验。这份贡献指南旨在帮助你熟悉代码库和我们的工作方式，让你可以尽快进入有趣的开发环节。\n\n索引：\n\n- [开发](#开发)\n  - [要求](#要求)\n  - [后端代码](#后端代码)\n  - [前端代码](#前端代码)\n- [提交 PR](#提交-pr)\n  - [提交流程](#提交流程)\n- [获取帮助](#获取帮助)\n\n---\n\n## 开发\n\n### 要求\n\n- Go 1.25+（用于修改后端代码）\n- Node.js 22.12+（用于修改前端代码）\n\n### 后端代码\n\nCertimate 的后端代码是使用 Golang 开发的，是一个基于 [Pocketbase](https://github.com/pocketbase/pocketbase) 构建的单体应用。\n\n假设你已经对 Certimate 的后端代码做出了一些修改，现在你想要运行它，请遵循以下步骤：\n\n1. 进入根目录；\n2. 安装依赖：\n   ```bash\n   go mod vendor\n   ```\n3. 启动本地开发服务：\n   ```bash\n   go run main.go serve\n   ```\n\n这将启动一个 Web 服务器，默认运行在 `http://localhost:8090`，并使用来自 `/ui/dist` 的预构建管理页面。\n\n> 如果你遇到报错 `ui/embed.go: pattern all:dist: no matching files found`，请参考“[前端代码](#前端代码)”这一小节构建 WebUI。\n\n**在向主仓库提交 PR 之前，你应该：**\n\n- 使用 [gofumpt](https://github.com/mvdan/gofumpt) 格式化你的代码。推荐使用 VSCode，并安装 gofumpt 插件，以便在保存时自动格式化。\n- 为你的改动添加单元测试或集成测试（使用 Go 标准库中的 `testing` 包）。\n\n### 前端代码\n\nCertimate 的前端代码是使用 TypeScript 开发的，是一个基于 [React](https://github.com/facebook/react) 和 [Vite](https://github.com/vitejs/vite) 构建的单页应用。\n\n假设你已经对 Certimate 的前端代码做出了一些修改，现在你想要运行它，请遵循以下步骤：\n\n1. 进入 `/ui` 目录；\n2. 安装依赖：\n   ```bash\n   npm install\n   ```\n3. 启动 Vite 开发服务器：\n   ```bash\n   npm run dev\n   ```\n\n这将启动一个 Web 服务器，默认运行在 `http://localhost:5173`，你可以通过浏览器访问来查看运行中的 WebUI。\n\n完成修改后，运行以下命令来构建 WebUI，以便它能被嵌入到 Go 包中：\n\n```bash\nnpm run build\n```\n\n**在向主仓库提交 PR 之前，你应该：**\n\n- 使用 [ESLint](https://github.com/eslint/eslint) 格式化你的代码。推荐使用 VSCode，并安装 ESLint 插件，以便在保存时自动格式化。\n\n## 提交 PR\n\n在提交 PR 之前，请先创建一个 Issue 来讨论你的修改方案，并等待来自项目维护者的反馈。这样做有助于：\n\n- 让我们充分理解你的修改内容；\n- 评估修改是否符合项目路线图；\n- 避免重复工作；\n- 防止你投入时间到可能无法被合并的修改中。\n\n### 提交流程\n\n1. Fork 本仓库并签出到 `main` 分支；\n2. 在提交 PR 之前，请先发起 Issue 讨论你想要做的修改；\n3. 为你的修改创建一个新的分支；\n4. 请为你的修改添加相应的测试；\n5. 确保你的代码能通过现有的测试；\n6. 请在 PR 描述中关联相关 Issue；\n7. 等待合并！\n\n> [!IMPORTANT]\n>\n> 建议为每个新功能或 Bug 修复创建一个从 `main` 分支派生的新分支。如果你计划提交多个 PR，请保持不同的改动在独立分支中，以便更容易进行代码审查并最终合并。\n>\n> 保持一个 PR 只实现一个功能或修复。\n\n## 获取帮助\n\n如果你在贡献过程中遇到困难或问题，可以通过 GitHub Issues 向我们提问。\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:24-alpine AS webui-builder\nWORKDIR /app\nCOPY . /app/\nRUN \\\n  cd /app/ui && \\\n  npm install && \\\n  npm run build\n\n\n\nFROM golang:1.25-alpine AS server-builder\nWORKDIR /app\nCOPY ../. /app/\nRUN rm -rf /app/ui/dist\nCOPY --from=webui-builder /app/ui/dist /app/ui/dist\nENV CGO_ENABLED=0\nRUN go build -trimpath -ldflags=\"-s -w\" -o certimate\n\n\n\nFROM alpine:latest\nWORKDIR /app\nCOPY --from=server-builder /app/certimate .\nENTRYPOINT [\"./certimate\", \"serve\", \"--http\", \"0.0.0.0:8090\"]\n"
  },
  {
    "path": "Dockerfile.gh",
    "content": "# Build docker image on GitHub Actions runner is too slow,\n# and it doesn't support armv7 (see https://github.com/parcel-bundler/lightningcss/issues/988).\n# So we pre-build webui, and just use simple Dockerfile here.\n\nFROM golang:1.25-alpine AS server-builder\nWORKDIR /app\nCOPY ../. /app/\nENV CGO_ENABLED=0\nRUN go build -trimpath -ldflags=\"-s -w\" -o certimate\n\n\n\nFROM alpine:latest\nWORKDIR /app\nCOPY --from=server-builder /app/certimate .\nENTRYPOINT [\"./certimate\", \"serve\", \"--http\", \"0.0.0.0:8090\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 certimate-go\nCopyright (c) 2024 Yoan.Liu\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "# 定义变量\nBINARY_NAME=certimate\nBUILD_DIR=build\n\n# 支持的操作系统和架构列表\nOS_ARCH=\\\n    linux/amd64 \\\n    linux/arm64 \\\n    darwin/amd64 \\\n    darwin/arm64 \\\n    windows/amd64 \\\n    windows/arm64\n\n# 默认目标\nall: build\n\n# 构建所有平台的二进制文件\nbuild: $(OS_ARCH)\n$(OS_ARCH):\n\t@mkdir -p $(BUILD_DIR)\n\tGOOS=$(word 1,$(subst /, ,$@)) \\\n\tGOARCH=$(word 2,$(subst /, ,$@)) \\\n\tCGO_ENABLED=0 \\\n\tgo build -trimpath -ldflags=\"-s -w\" -o $(BUILD_DIR)/$(BINARY_NAME)_$(word 1,$(subst /, ,$@))_$(word 2,$(subst /, ,$@)) .\n\n# 清理构建文件\nclean:\n\trm -rf $(BUILD_DIR)\n\n# 帮助信息\nhelp:\n\t@echo \"Usage:\"\n\t@echo \"  make        - 编译所有平台的二进制文件\"\n\t@echo \"  make clean  - 清理构建文件\"\n\t@echo \"  make help   - 显示此帮助信息\"\n\n.PHONY: all build clean help\n\nlocal.run:\n\tgo mod vendor&& npm --prefix=./ui install && npm --prefix=./ui run build && go run main.go serve --http 127.0.0.1:8090\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">🔒 Certimate</h1>\n\n<div align=\"center\">\n\n[![Stars](https://img.shields.io/github/stars/certimate-go/certimate?style=flat)](https://github.com/certimate-go/certimate)\n[![Forks](https://img.shields.io/github/forks/certimate-go/certimate?style=flat)](https://github.com/certimate-go/certimate)\n[![Docker Pulls](https://img.shields.io/docker/pulls/certimate/certimate?style=flat)](https://hub.docker.com/r/certimate/certimate)\n[![Release](https://img.shields.io/github/v/release/certimate-go/certimate?style=flat&sort=semver)](https://github.com/certimate-go/certimate/releases)\n[![License](https://img.shields.io/github/license/certimate-go/certimate?style=flat)](https://mit-license.org/)\n[![Ask DeepWiki](https://deepwiki.com/badge.svg?label=DeepWiki)](https://deepwiki.com/certimate-go/certimate)\n\n</div>\n\n<div align=\"center\">\n\nEnglish ｜ [简体中文](README_zh.md)\n\n</div>\n\n---\n\n## 🚩 Introduction\n\nAn open-source and free self-hosted SSL certificates ACME tool, automates the full-cycle of issuance, deployment, renewal, and monitoring visually.\n\n- **Self-Hosted**: Private deployment. All data is stored locally, giving you full control to ensure data privacy and security.\n- **Zero Dependencies**: No need to install databases, runtimes, or any complex frameworks. Truly ready to use out of the box with a single click.\n- **Low Resource Usage**: Extremely lightweight, requiring only ~16 MB of memory. It's so efficient that it can even run on devices like home routers.\n- **Easy to Use**: The user-friendly GUI lets you automate certificate management for multiple platforms with a visual workflow — all with just a few simple configurations.\n\n## 💡 Features\n\n- Flexible workflow orchestration, fully automation from certificate application to deployment.\n- Supports requesting single/multiple/wildcard domain certificates, IP address certificates, with options for RSA or ECC key.\n- Supports DNS-01 challenge and HTTP-01 challenge both.\n- Supports various certificate formats such as PEM, PFX, JKS.\n- Supports more than 60+ domain registrars (e.g., AWS, Cloudflare, GoDaddy, Alibaba Cloud, Tencent Cloud, etc. [Check out full providers](https://docs.certimate.me/en-US/docs/reference/providers#supported-dns-providers)).\n- Supports more than 110+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out full providers](https://docs.certimate.me/en-US/docs/reference/providers#supported-hosting-providers)).\n- Supports multiple notification channels including email, Discord, Slack, Telegram, DingTalk, Feishu, WeCom, and more.\n- Supports multiple ACME CAs including Let's Encrypt, Actalis, Google Trust Services, SSL.com, ZeroSSL, and more.\n- More features waiting to be discovered.\n\n## 🚀 Quick Start\n\n**Run Certimate in 1 minute!**\n\n<details>\n<summary>👉 Binary Installation: </summary>\n\nDownload the archived package of precompiled executable files directly from [GitHub Releases](https://github.com/certimate-go/certimate/releases), extract and then execute:\n\n```bash\n./certimate serve\n```\n\n</details>\n\n<details>\n<summary>👉 Docker Installation: </summary>\n\n```bash\ndocker run -d \\\n  --name certimate \\\n  --restart unless-stopped \\\n  -p 8090:8090 \\\n  -v /etc/localtime:/etc/localtime:ro \\\n  -v /etc/timezone:/etc/timezone:ro \\\n  -v $(pwd)/data:/app/pb_data \\\n  certimate/certimate:latest\n```\n\n</details>\n\nVisit `http://127.0.0.1:8090` in your browser.\n\nDefault administrator account:\n\n- Username: `admin@certimate.fun`\n- Password: `1234567890`\n\nWork with Certimate right now. Or read other content in the documentation to learn more.\n\n## 📄 Documentation\n\nFor full documentation, please visit [docs.certimate.me](https://docs.certimate.me/).\n\nRelated articles:\n\n> - [_Migrate to v0.4_](https://docs.certimate.me/en-US/docs/migrations/migrate-to-v0.4)\n> - [_使用 CNAME 完成 ACME DNS-01 质询_](https://docs.certimate.me/en-US/blog/cname)\n> - [_Why Certimate?_](https://docs.certimate.me/en-US/blog/why-certimate)\n\n## 🖥️ Screenshot\n\n[![Screenshot](https://i.imgur.com/4DAUKEE.gif)](https://www.youtube.com/watch?v=am_yzdfyNOE)\n\n## 🤝 Contributing\n\nCertimate is a free and open-source project, and your feedback and contributions are needed and always welcome. Contributions include but are not limited to: submitting code, reporting bugs, sharing ideas, or showcasing your use cases based on Certimate. We also encourage users to share Certimate on personal blogs or social media.\n\nFor those who'd like to contribute code, see our [Contribution Guide](./CONTRIBUTING_EN.md).\n\n[Issues](https://github.com/certimate-go/certimate/issues) and [Pull Requests](https://github.com/certimate-go/certimate/pulls) are opened at https://github.com/certimate-go/certimate.\n\n#### Contributors\n\n[![Contributors](https://contrib.rocks/image?repo=certimate-go/certimate)](https://github.com/certimate-go/certimate/graphs/contributors)\n\n## ⛔ Disclaimer\n\nThis repository is available under the [MIT License](https://opensource.org/licenses/MIT), and distributed “as-is” without any warranty of any kind. The authors and contributors are not responsible for any damages or losses resulting from the use or inability to use this software, including but not limited to data loss, business interruption, or any other potential harm.\n\n**No Warranties**: This software comes without any express or implied warranties, including but not limited to implied warranties of merchantability, fitness for a particular purpose, and non-infringement.\n\n**User Responsibilities**: By using this software, you agree to take full responsibility for any outcomes resulting from its use.\n\n## 🌐 Join the Community\n\n- [Telegram](https://t.me/+ZXphsppxUg41YmVl)\n- Wechat Group (contact to the author [@usual2970](https://github.com/usual2970) to getting invitation)\n\n  <img src=\"https://i.imgur.com/8xwsLTA.png\" width=\"200\"/>\n\n## ⭐ Star History\n\nStar Certificate on GitHub and be instantly notified of new releases.\n\n[![Stargazers over time](https://starchart.cc/certimate-go/certimate.svg?variant=adaptive)](https://starchart.cc/certimate-go/certimate)\n"
  },
  {
    "path": "README_zh.md",
    "content": "<h1 align=\"center\">🔒 Certimate</h1>\n\n<div align=\"center\">\n\n[![Stars](https://img.shields.io/github/stars/certimate-go/certimate?style=flat)](https://github.com/certimate-go/certimate)\n[![Forks](https://img.shields.io/github/forks/certimate-go/certimate?style=flat)](https://github.com/certimate-go/certimate)\n[![Docker Pulls](https://img.shields.io/docker/pulls/certimate/certimate?style=flat)](https://hub.docker.com/r/certimate/certimate)\n[![Release](https://img.shields.io/github/v/release/certimate-go/certimate?style=flat&sort=semver)](https://github.com/certimate-go/certimate/releases)\n[![License](https://img.shields.io/github/license/certimate-go/certimate?style=flat)](https://mit-license.org/)\n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/certimate-go/certimate)\n\n</div>\n\n<div align=\"center\">\n\n[English](README.md) ｜ 简体中文\n\n</div>\n\n---\n\n## 🚩 项目简介\n\n完全开源免费的自托管 SSL 证书 ACME 工具，申请、部署、续期、监控全流程自动化可视化，支持各大主流云厂商。\n\n- **自托管**：私有化部署，所有数据本地化存储，掌控数据的隐私与安全。\n- **零依赖**：无需安装数据库、运行时或复杂框架，一键启动，开箱即用。\n- **低占用**：超轻量的资源开销，仅需 ~16 MB 内存，甚至可以运行在家用路由器。\n- **易操作**：图形化界面，通过简单配置即可完成证书申请、部署和续期的自动化工作。\n\n## 💡 功能特性\n\n- 灵活的工作流编排方式，证书从申请到部署完全自动化。\n- 支持申请单/多/泛域名证书、IP 地址证书，可选 RSA、ECC 私钥算法。\n- 支持 DNS-01（即基于域名解析验证）、HTTP-01（即基于文件验证）两种质询方式。\n- 支持 PEM、PFX、JKS 等多种格式输出证书。\n- 支持 60+ 域名托管商（如阿里云、腾讯云、AWS、Cloudflare、GoDaddy 等，[点此查看完整清单](https://docs.certimate.me/zh-CN/docs/reference/providers#supported-dns-providers)）。\n- 支持 110+ 部署目标（如 Kubernetes、CDN、WAF、负载均衡等，[点此查看完整清单](https://docs.certimate.me/zh-CN/docs/reference/providers#supported-hosting-providers)）。\n- 支持邮件、钉钉、飞书、企业微信、Discord、Slack、Telegram 等多种通知渠道。\n- 支持 Let's Encrypt、Actalis、Google Trust Services、SSL.com、ZeroSSL 等多种 ACME 证书颁发机构。\n- 更多特性等待探索。\n\n## 🚀 快速启动\n\n**1 分钟运行 Certimate！**\n\n<details>\n<summary>👉 二进制安装：</summary>\n\n从 [GitHub Releases](https://github.com/certimate-go/certimate/releases) 页面下载预先编译好的可执行文件压缩包，解压缩后在终端中执行：\n\n```bash\n./certimate serve\n```\n\n</details>\n\n<details>\n<summary>👉 Docker 安装：</summary>\n\n```bash\ndocker run -d \\\n  --name certimate \\\n  --restart unless-stopped \\\n  -p 8090:8090 \\\n  -v /etc/localtime:/etc/localtime:ro \\\n  -v /etc/timezone:/etc/timezone:ro \\\n  -v $(pwd)/data:/app/pb_data \\\n  certimate/certimate:latest\n```\n\n</details>\n\n浏览器中访问 `http://127.0.0.1:8090`。\n\n初始的管理员账号及密码：\n\n- 账号：`admin@certimate.fun`\n- 密码：`1234567890`\n\n即刻使用 Certimate。或者阅读文档中的其他内容以了解更多。\n\n## 📄 使用手册\n\n请访问文档站 [docs.certimate.me](https://docs.certimate.me/) 以阅读使用手册。\n\n> （由于众所周知的原因，中国大陆用户可能需要 🪄 上网才能访问文档站。）\n\n相关文章：\n\n> - [《升级指南：迁移到 v0.4》](https://docs.certimate.me/zh-CN/docs/migrations/migrate-to-v0.4)\n> - [《使用 CNAME 完成 ACME DNS-01 质询》](https://docs.certimate.me/zh-CN/blog/cname)\n> - [《Why Certimate?》](https://docs.certimate.me/zh-CN/blog/why-certimate)\n\n## 🖥️ 运行界面\n\n[![Screenshot](https://i.imgur.com/4DAUKEE.gif)](https://www.bilibili.com/video/BV1xockeZEm2)\n\n## 🤝 参与贡献\n\nCertimate 是一个免费且开源的项目。我们欢迎任何人为 Certimate 做出贡献，以帮助改善 Certimate。包括但不限于：提交代码、反馈缺陷、交流想法，或分享你基于 Certimate 的使用案例。同时，我们也欢迎用户在个人博客或社交媒体上分享 Certimate。\n\n如果你想要贡献代码，请先阅读我们的[贡献指南](./CONTRIBUTING.md)。\n\n请在 https://github.com/certimate-go/certimate 提交 [Issues](https://github.com/certimate-go/certimate/issues) 和 [Pull Requests](https://github.com/certimate-go/certimate/pulls)。\n\n#### 感谢以下贡献者对 Certimate 做出的贡献：\n\n[![Contributors](https://contrib.rocks/image?repo=certimate-go/certimate)](https://github.com/certimate-go/certimate/graphs/contributors)\n\n## ⛔ 免责声明\n\nCertimate 遵循 [MIT License](https://opensource.org/licenses/MIT) 开源协议，完全免费提供，旨在“按现状”供用户使用。作者及贡献者不对使用本软件所产生的任何直接或间接后果承担责任，包括但不限于性能下降、数据丢失、服务中断、或任何其他类型的损害。\n\n**无任何保证**：本软件不提供任何明示或暗示的保证，包括但不限于对特定用途的适用性、无侵权性、商用性及可靠性的保证。\n\n**用户责任**：使用本软件即表示您理解并同意承担由此产生的一切风险及责任。\n\n## 🌐 加入社群\n\n- [Telegram](https://t.me/+ZXphsppxUg41YmVl)\n- 微信群聊（因微信自身限制需群主邀请，可先加 [@usual2970](https://github.com/usual2970) 好友）\n\n  <img src=\"https://i.imgur.com/8xwsLTA.png\" width=\"200\"/>\n\n## ⭐ 星标趋势\n\n在 GitHub 上为 Certimate 添加 Star 星标关注，即可第一时间获取新版本发布通知。\n\n[![Stargazers over time](https://starchart.cc/certimate-go/certimate.svg?variant=adaptive)](https://starchart.cc/certimate-go/certimate)\n"
  },
  {
    "path": "cmd/intercmd.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/go-acme/lego/v4/lego\"\n\tlegolog \"github.com/go-acme/lego/v4/log\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/certacme\"\n\t\"github.com/certimate-go/certimate/internal/tools/mproc\"\n)\n\nfunc NewInternalCommand(app core.App) *cobra.Command {\n\tcommand := &cobra.Command{\n\t\tUse:   \"intercmd\",\n\t\tShort: \"[INTERNAL] Internal dedicated for Certimate\",\n\t}\n\n\tcommand.AddCommand(internalCertApplyCommand(app))\n\n\treturn command\n}\n\nfunc internalCertApplyCommand(_ core.App) *cobra.Command {\n\tvar flagInput string\n\tvar flagOutput string\n\tvar flagError string\n\tvar flagEncryptionKey string\n\n\tcommand := &cobra.Command{\n\t\tUse:          \"certapply\",\n\t\tExample:      \"internal certapply --in ./in.file --out ./out.file --enckey aeskey\",\n\t\tSilenceUsage: true,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\ttype InData struct {\n\t\t\t\tAccount *certacme.ACMEAccount              `json:\"account,omitempty\"`\n\t\t\t\tRequest *certacme.ObtainCertificateRequest `json:\"request,omitempty\"`\n\t\t\t}\n\n\t\t\ttype OutData struct {\n\t\t\t\tResponse *certacme.ObtainCertificateResponse `json:\"response\"`\n\t\t\t}\n\n\t\t\tmreceiver := mproc.NewReceiver(func(ctx context.Context, params *InData) (*OutData, error) {\n\t\t\t\tif params.Account == nil {\n\t\t\t\t\treturn nil, errors.New(\"illegal params\")\n\t\t\t\t}\n\t\t\t\tif params.Request == nil {\n\t\t\t\t\treturn nil, errors.New(\"illegal params\")\n\t\t\t\t}\n\n\t\t\t\t// redirect to stdout, remove datetime prefix\n\t\t\t\t// so that the logger can split logs correctly\n\t\t\t\t// see: /internal/tools/mproc/sender.go\n\t\t\t\tlegolog.Logger = log.New(os.Stdout, \"\", 0)\n\n\t\t\t\tclient, err := certacme.NewACMEClientWithAccount(params.Account, func(c *lego.Config) error {\n\t\t\t\t\tc.UserAgent = app.AppUserAgent\n\t\t\t\t\tc.Certificate.KeyType = params.Request.PrivateKeyType\n\t\t\t\t\tc.Certificate.DisableCommonName = params.Request.NoCommonName\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to initialize acme client: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tresp, err := client.ObtainCertificate(ctx, params.Request)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to obtain certificate: %w\", err)\n\t\t\t\t}\n\n\t\t\t\treturn &OutData{\n\t\t\t\t\tResponse: resp,\n\t\t\t\t}, nil\n\t\t\t})\n\t\t\tif err := mreceiver.ReceiveWithContext(cmd.Context(), flagInput, flagOutput, flagEncryptionKey); err != nil {\n\t\t\t\tos.WriteFile(flagError, []byte(err.Error()), 0o644)\n\t\t\t}\n\t\t},\n\t}\n\n\tcommand.PersistentFlags().StringVar(&flagInput, \"in\", \"\", \"\")\n\tcommand.PersistentFlags().StringVar(&flagOutput, \"out\", \"\", \"\")\n\tcommand.PersistentFlags().StringVar(&flagError, \"err\", \"\", \"\")\n\tcommand.PersistentFlags().StringVar(&flagEncryptionKey, \"enckey\", \"\", \"\")\n\n\treturn command\n}\n"
  },
  {
    "path": "cmd/serve_nonwindows.go",
    "content": "//go:build !windows\n// +build !windows\n\npackage cmd\n\nimport (\n\t\"github.com/pocketbase/pocketbase\"\n)\n\nfunc Serve(app *pocketbase.PocketBase) error {\n\treturn app.Start()\n}\n"
  },
  {
    "path": "cmd/serve_windows.go",
    "content": "//go:build windows\n// +build windows\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pocketbase/pocketbase\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"golang.org/x/sys/windows/svc\"\n\t\"golang.org/x/sys/windows/svc/eventlog\"\n)\n\ntype winscHandler struct {\n\tpb   *pocketbase.PocketBase\n\telog *eventlog.Log\n}\n\nfunc (h *winscHandler) Execute(args []string, r <-chan svc.ChangeRequest, s chan<- svc.Status) (bool, uint32) {\n\tgo func() {\n\t\tif err := h.pb.Start(); err != nil {\n\t\t\th.elog.Error(999, fmt.Sprintf(\"Start failed: %v\", err))\n\t\t}\n\t}()\n\n\ts <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}\n\tfor {\n\t\tselect {\n\t\tcase c := <-r:\n\t\t\tswitch c.Cmd {\n\t\t\tcase svc.Interrogate:\n\t\t\t\ts <- c.CurrentStatus\n\t\t\tcase svc.Stop, svc.Shutdown:\n\t\t\t\tevent := new(core.TerminateEvent)\n\t\t\t\tevent.App = h.pb\n\t\t\t\th.pb.OnTerminate().Trigger(event, func(e *core.TerminateEvent) error {\n\t\t\t\t\treturn e.App.ResetBootstrapState()\n\t\t\t\t})\n\t\t\t\ts <- svc.Status{State: svc.Stopped}\n\t\t\t\treturn false, 0\n\t\t\tdefault:\n\t\t\t\th.elog.Warning(998, fmt.Sprintf(\"unexpected control request: %v\", c.Cmd))\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc Serve(app *pocketbase.PocketBase) error {\n\tif isWinsc, _ := svc.IsWindowsService(); isWinsc {\n\t\telog, _ := eventlog.Open(winscName)\n\t\tdefer elog.Close()\n\t\treturn svc.Run(winscName, &winscHandler{pb: app, elog: elog})\n\t}\n\n\treturn app.Start()\n}\n"
  },
  {
    "path": "cmd/winsc_nonwindows.go",
    "content": "//go:build !windows\n// +build !windows\n\npackage cmd\n\nimport (\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc NewWinscCommand(app core.App) *cobra.Command {\n\tcommand := &cobra.Command{\n\t\tUse:   \"winsc\",\n\t\tShort: \"Install/Uninstall Windows service (Not supported on non-Windows OS)\",\n\t}\n\n\treturn command\n}\n"
  },
  {
    "path": "cmd/winsc_windows.go",
    "content": "//go:build windows\n// +build windows\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/spf13/cobra\"\n\t\"golang.org/x/sys/windows/svc\"\n\t\"golang.org/x/sys/windows/svc/eventlog\"\n\t\"golang.org/x/sys/windows/svc/mgr\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\nconst winscName = \"certimate\"\n\nfunc NewWinscCommand(app core.App) *cobra.Command {\n\tcommand := &cobra.Command{\n\t\tUse:   \"winsc\",\n\t\tShort: \"Install/Uninstall Windows service\",\n\t}\n\n\tcommand.AddCommand(winscInstallCommand(app))\n\tcommand.AddCommand(winscUninstallCommand(app))\n\tcommand.AddCommand(winscStartCommand(app))\n\tcommand.AddCommand(winscStopCommand(app))\n\n\treturn command\n}\n\nfunc winscInstallCommand(_ core.App) *cobra.Command {\n\tcommand := &cobra.Command{\n\t\tUse:     \"install [args...]\",\n\t\tExample: \"winsc install\",\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tsrvPath, err := os.Executable()\n\t\t\tif err != nil {\n\t\t\t\tsrvPath = os.Args[0]\n\t\t\t}\n\n\t\t\tsrvArgs := []string{\"serve\"}\n\t\t\tsrvArgs = append(srvArgs, args...)\n\n\t\t\tmanager, err := mgr.Connect()\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(fmt.Sprintf(\"failed to connect to service manager: %v\", err))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer manager.Disconnect()\n\n\t\t\tconfig := mgr.Config{\n\t\t\t\tDisplayName: app.AppName,\n\t\t\t\tDescription: \"https://github.com/certimate-go/certimate\",\n\t\t\t\tStartType:   mgr.StartAutomatic,\n\t\t\t}\n\t\t\tservice, err := manager.CreateService(winscName, srvPath, config, srvArgs...)\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(fmt.Sprintf(\"failed to create service: %v\", err))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer service.Close()\n\n\t\t\teventlog.InstallAsEventCreate(winscName, eventlog.Error|eventlog.Warning|eventlog.Info)\n\t\t\tslog.Info(fmt.Sprintf(\"service '%s' installed\", winscName))\n\n\t\t\tif err := service.Start(); err != nil {\n\t\t\t\tslog.Warn(fmt.Sprintf(\"failed to start service: %v\", err))\n\t\t\t}\n\n\t\t\tslog.Info(fmt.Sprintf(\"service '%s' started\", winscName))\n\t\t},\n\t\tDisableFlagParsing: true,\n\t}\n\n\treturn command\n}\n\nfunc winscUninstallCommand(_ core.App) *cobra.Command {\n\tcommand := &cobra.Command{\n\t\tUse:     \"uninstall\",\n\t\tExample: \"winsc uninstall\",\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tmanager, err := mgr.Connect()\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(fmt.Sprintf(\"failed to connect to service manager: %v\", err))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer manager.Disconnect()\n\n\t\t\tservice, err := manager.OpenService(winscName)\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(fmt.Sprintf(\"failed to open service: %v\", err))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer service.Close()\n\n\t\t\tstatus, err := service.Query()\n\t\t\tif err == nil && status.State != svc.Stopped {\n\t\t\t\t_, err = service.Control(svc.Stop)\n\t\t\t\tif err != nil {\n\t\t\t\t\tslog.Warn(fmt.Sprintf(\"failed to stop service: %v\", err))\n\t\t\t\t}\n\n\t\t\t\ttime.Sleep(3 * time.Second)\n\t\t\t\tslog.Info(fmt.Sprintf(\"service '%s' stopped\", winscName))\n\t\t\t}\n\n\t\t\tif err = service.Delete(); err != nil {\n\t\t\t\tslog.Error(fmt.Sprintf(\"failed to delete service: %v\", err))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\teventlog.Remove(winscName)\n\t\t\tslog.Info(fmt.Sprintf(\"service '%s' uninstalled\", winscName))\n\t\t},\n\t}\n\n\treturn command\n}\n\nfunc winscStartCommand(_ core.App) *cobra.Command {\n\tcommand := &cobra.Command{\n\t\tUse:     \"start\",\n\t\tExample: \"winsc start\",\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tmanager, err := mgr.Connect()\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(fmt.Sprintf(\"failed to connect to service manager: %v\", err))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer manager.Disconnect()\n\n\t\t\tservice, err := manager.OpenService(winscName)\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(fmt.Sprintf(\"failed to open service: %v\", err))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer service.Close()\n\n\t\t\tif err := service.Start(); err != nil {\n\t\t\t\tslog.Error(fmt.Sprintf(\"failed to start service: %v\", err))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tslog.Info(fmt.Sprintf(\"service '%s' started\", winscName))\n\t\t},\n\t}\n\n\treturn command\n}\n\nfunc winscStopCommand(app core.App) *cobra.Command {\n\tcommand := &cobra.Command{\n\t\tUse:     \"stop\",\n\t\tExample: \"winsc stop\",\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tmanager, err := mgr.Connect()\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(fmt.Sprintf(\"failed to connect to service manager: %v\", err))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer manager.Disconnect()\n\n\t\t\tservice, err := manager.OpenService(winscName)\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(fmt.Sprintf(\"failed to open service: %v\", err))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer service.Close()\n\n\t\t\tstatus, err := service.Query()\n\t\t\tif err == nil && status.State != svc.Stopped {\n\t\t\t\t_, err = service.Control(svc.Stop)\n\t\t\t\tif err != nil {\n\t\t\t\t\tslog.Warn(fmt.Sprintf(\"failed to stop service: %v\", err))\n\t\t\t\t}\n\n\t\t\t\ttime.Sleep(3 * time.Second)\n\t\t\t\tslog.Info(fmt.Sprintf(\"service '%s' stopped\", winscName))\n\t\t\t}\n\t\t},\n\t}\n\n\treturn command\n}\n"
  },
  {
    "path": "docker/docker-compose.yml",
    "content": "version: \"3.0\"\nservices:\n  certimate:\n    image: certimate/certimate:latest\n    container_name: certimate\n    ports:\n      - 8090:8090\n    volumes:\n      - /etc/localtime:/etc/localtime:ro\n      - /etc/timezone:/etc/timezone:ro\n      - ./data:/app/pb_data\n    restart: unless-stopped\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/certimate-go/certimate\n\ngo 1.25.0\n\ntoolchain go1.25.5\n\nrequire (\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1\n\tgithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.4.0\n\tgithub.com/G-Core/gcorelabscdn-go v1.0.35\n\tgithub.com/KscSDK/ksc-sdk-go v0.18.0\n\tgithub.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0\n\tgithub.com/alibabacloud-go/alb-20200616/v2 v2.3.1\n\tgithub.com/alibabacloud-go/apig-20240327/v6 v6.0.1\n\tgithub.com/alibabacloud-go/cas-20200407/v4 v4.1.0\n\tgithub.com/alibabacloud-go/cdn-20180510/v9 v9.0.0\n\tgithub.com/alibabacloud-go/cloudapi-20160714/v5 v5.7.9\n\tgithub.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15\n\tgithub.com/alibabacloud-go/dcdn-20180115/v4 v4.1.0\n\tgithub.com/alibabacloud-go/ddoscoo-20200101/v5 v5.0.1\n\tgithub.com/alibabacloud-go/esa-20240910/v2 v2.48.0\n\tgithub.com/alibabacloud-go/fc-20230330/v4 v4.6.8\n\tgithub.com/alibabacloud-go/fc-open-20210406/v2 v2.0.12\n\tgithub.com/alibabacloud-go/ga-20191120/v4 v4.0.0\n\tgithub.com/alibabacloud-go/live-20161101/v2 v2.6.0\n\tgithub.com/alibabacloud-go/nlb-20220430/v4 v4.1.2\n\tgithub.com/alibabacloud-go/openapi-util v0.1.1\n\tgithub.com/alibabacloud-go/slb-20140515/v4 v4.0.13\n\tgithub.com/alibabacloud-go/tea v1.4.0\n\tgithub.com/alibabacloud-go/tea-utils/v2 v2.0.9\n\tgithub.com/alibabacloud-go/vod-20170321/v4 v4.11.1\n\tgithub.com/alibabacloud-go/waf-openapi-20211001/v7 v7.5.0\n\tgithub.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.4.0\n\tgithub.com/aws/aws-sdk-go-v2 v1.41.2\n\tgithub.com/aws/aws-sdk-go-v2/config v1.32.10\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.19.10\n\tgithub.com/aws/aws-sdk-go-v2/service/acm v1.37.20\n\tgithub.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.1\n\tgithub.com/aws/aws-sdk-go-v2/service/iam v1.53.3\n\tgithub.com/baidubce/bce-sdk-go v0.9.260\n\tgithub.com/byteplus-sdk/byteplus-sdk-golang v1.0.62\n\tgithub.com/go-acme/lego/v4 v4.32.0\n\tgithub.com/go-cmd/cmd v1.4.3\n\tgithub.com/go-resty/resty/v2 v2.17.1\n\tgithub.com/go-viper/mapstructure/v2 v2.5.0\n\tgithub.com/google/go-querystring v1.2.0\n\tgithub.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187\n\tgithub.com/jdcloud-api/jdcloud-sdk-go v1.64.0\n\tgithub.com/kong/go-kong v0.72.1\n\tgithub.com/luthermonson/go-proxmox v0.3.2\n\tgithub.com/microcosm-cc/bluemonday v1.0.27\n\tgithub.com/minio/minio-go/v7 v7.0.98\n\tgithub.com/mohuatech/mohuacloud-go-sdk v0.0.0-20251115182757-6fba4d0a4c47\n\tgithub.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0\n\tgithub.com/pkg/sftp v1.13.10\n\tgithub.com/pocketbase/dbx v1.12.0\n\tgithub.com/pocketbase/pocketbase v0.36.5\n\tgithub.com/povsister/scp v0.0.0-20250701154629-777cf82de5df\n\tgithub.com/pquerna/otp v1.5.0\n\tgithub.com/qiniu/go-sdk/v7 v7.25.6\n\tgithub.com/samber/lo v1.52.0\n\tgithub.com/spf13/cobra v1.10.2\n\tgithub.com/spf13/pflag v1.0.10\n\tgithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.3.36\n\tgithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.3.45\n\tgithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48\n\tgithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap v1.3.34\n\tgithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.3.45\n\tgithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf v1.3.29\n\tgithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.3.42\n\tgithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.3.45\n\tgithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod v1.3.46\n\tgithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf v1.3.46\n\tgithub.com/ucloud/ucloud-sdk-go v0.22.59\n\tgithub.com/volcengine/ve-tos-golang-sdk/v2 v2.9.1\n\tgithub.com/volcengine/volc-sdk-golang v1.0.237\n\tgithub.com/volcengine/volcengine-go-sdk v1.2.15\n\tgithub.com/wneessen/go-mail v0.7.2\n\tgithub.com/xhit/go-str2duration/v2 v2.1.0\n\tgitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1\n\tgitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0\n\tgolang.org/x/crypto v0.48.0\n\tgolang.org/x/sync v0.19.0\n\tgolang.org/x/sys v0.41.0\n\tk8s.io/api v0.35.2\n\tk8s.io/apimachinery v0.35.2\n\tk8s.io/client-go v0.35.2\n\tsoftware.sslmate.com/src/go-pkcs12 v0.7.0\n)\n\nrequire (\n\tgithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect\n\tgithub.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect\n\tgithub.com/alibabacloud-go/alibabacloud-gateway-fc-util v0.0.7 // indirect\n\tgithub.com/avast/retry-go v3.0.0+incompatible // indirect\n\tgithub.com/aws/aws-sdk-go v1.40.45 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 // indirect\n\tgithub.com/benbjohnson/clock v1.3.5 // indirect\n\tgithub.com/buger/goterm v1.0.4 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/diskfs/go-diskfs v1.7.0 // indirect\n\tgithub.com/djherbis/times v1.6.0 // indirect\n\tgithub.com/emicklei/go-restful/v3 v3.12.2 // indirect\n\tgithub.com/fxamacker/cbor/v2 v2.9.0 // indirect\n\tgithub.com/go-acme/alidns-20150109/v4 v4.7.0 // indirect\n\tgithub.com/go-acme/tencentclouddnspod v1.3.24 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.21.0 // indirect\n\tgithub.com/go-openapi/jsonreference v0.21.0 // indirect\n\tgithub.com/go-openapi/swag v0.23.0 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.23.0 // indirect\n\tgithub.com/go-sql-driver/mysql v1.8.1 // indirect\n\tgithub.com/goccy/go-yaml v1.9.8 // indirect\n\tgithub.com/gofrs/uuid v4.4.0+incompatible // indirect\n\tgithub.com/golang-jwt/jwt/v5 v5.3.1 // indirect\n\tgithub.com/google/gnostic-models v0.7.0 // indirect\n\tgithub.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/go-retryablehttp v0.7.8 // indirect\n\tgithub.com/imdario/mergo v0.3.12 // indirect\n\tgithub.com/jinzhu/copier v0.4.0 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/kong/semver/v4 v4.0.1 // indirect\n\tgithub.com/kylelemons/godebug v1.1.0 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/linode/linodego v1.65.0 // indirect\n\tgithub.com/magefile/mage v1.15.0 // indirect\n\tgithub.com/mailru/easyjson v0.9.0 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/namedotcom/go/v4 v4.0.2 // indirect\n\tgithub.com/nrdcg/bunny-go v0.1.0 // indirect\n\tgithub.com/nrdcg/desec v0.11.1 // indirect\n\tgithub.com/nrdcg/goacmedns v0.2.0 // indirect\n\tgithub.com/nrdcg/porkbun v0.4.0 // indirect\n\tgithub.com/ovh/go-ovh v1.9.0 // indirect\n\tgithub.com/peterhellberg/link v1.2.0 // indirect\n\tgithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/qiniu/dyn v1.3.0 // indirect\n\tgithub.com/qiniu/x v1.10.5 // indirect\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect\n\tgithub.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect\n\tgithub.com/stretchr/objx v0.5.2 // indirect\n\tgithub.com/stretchr/testify v1.11.1 // indirect\n\tgithub.com/tidwall/gjson v1.18.0 // indirect\n\tgithub.com/tidwall/match v1.1.1 // indirect\n\tgithub.com/tidwall/pretty v1.2.0 // indirect\n\tgithub.com/vultr/govultr/v3 v3.27.0 // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgo.mongodb.org/mongo-driver v1.17.2 // indirect\n\tgo.uber.org/ratelimit v0.3.1 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.3 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect\n\tgolang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect\n\tgopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect\n\tgopkg.in/inf.v0 v0.9.1 // indirect\n\tgopkg.in/ns1/ns1-go.v2 v2.17.2 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tk8s.io/klog/v2 v2.130.1 // indirect\n\tk8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect\n\tk8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect\n\tsigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect\n\tsigs.k8s.io/randfill v1.0.0 // indirect\n\tsigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect\n\tsigs.k8s.io/yaml v1.6.0 // indirect\n)\n\nrequire (\n\tgithub.com/BurntSushi/toml v1.6.0 // indirect\n\tgithub.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect\n\tgithub.com/alibabacloud-go/debug v1.0.1 // indirect\n\tgithub.com/alibabacloud-go/endpoint-util v1.1.1 // indirect\n\tgithub.com/aliyun/credentials-go v1.4.7 // indirect\n\tgithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect\n\tgithub.com/aws/smithy-go v1.24.1 // indirect\n\tgithub.com/aymerick/douceur v0.2.0 // indirect\n\tgithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.3.0 // indirect\n\tgithub.com/clbanning/mxj/v2 v2.7.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/disintegration/imaging v1.6.2 // indirect\n\tgithub.com/domodwyer/mailyak/v3 v3.6.2 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/fatih/color v1.18.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.13 // indirect\n\tgithub.com/ganigeorgiev/fexpr v0.5.0 // indirect\n\tgithub.com/go-acme/esa-20240910/v2 v2.48.0 // indirect\n\tgithub.com/go-acme/jdcloud-sdk-go v1.64.0 // indirect\n\tgithub.com/go-acme/tencentedgdeone v1.3.38 // indirect\n\tgithub.com/go-ini/ini v1.67.0 // indirect\n\tgithub.com/go-jose/go-jose/v4 v4.1.3 // indirect\n\tgithub.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/gorilla/css v1.0.1 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/jmespath/go-jmespath v0.4.0 // indirect\n\tgithub.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect\n\tgithub.com/klauspost/compress v1.18.2 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.11 // indirect\n\tgithub.com/klauspost/crc32 v1.3.0 // indirect\n\tgithub.com/kr/fs v0.1.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/miekg/dns v1.1.72 // indirect\n\tgithub.com/minio/crc64nvme v1.1.1 // indirect\n\tgithub.com/minio/md5-simd v1.1.2 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect\n\tgithub.com/ncruces/go-strftime v1.0.0 // indirect\n\tgithub.com/nrdcg/namesilo v0.5.0 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/rs/xid v1.6.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/tinylib/msgp v1.6.1 // indirect\n\tgithub.com/tjfoc/gmsm v1.4.1 // indirect\n\tgolang.org/x/image v0.36.0 // indirect\n\tgolang.org/x/mod v0.32.0 // indirect\n\tgolang.org/x/net v0.50.0 // indirect\n\tgolang.org/x/oauth2 v0.35.0 // indirect\n\tgolang.org/x/term v0.40.0 // indirect\n\tgolang.org/x/text v0.34.0 // indirect\n\tgolang.org/x/time v0.14.0 // indirect\n\tgolang.org/x/tools v0.41.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\tgopkg.in/ini.v1 v1.67.1 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tmodernc.org/libc v1.67.6 // indirect\n\tmodernc.org/mathutil v1.7.1 // indirect\n\tmodernc.org/memory v1.11.0 // indirect\n\tmodernc.org/sqlite v1.46.1 // indirect\n)\n\nreplace gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 => ./pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0\n\nreplace gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1 => ./pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\nfilippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 h1:yzrctSl9GMIQ5lHu7jc8olOsGjWDCsBpJhWqfGa/YIM=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0/go.mod h1:GE4m0rnnfwLGX0Y9A9A25Zx5N/90jneT5ABevqzhuFQ=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 h1:zLzoX5+W2l95UJoVwiyNS4dX8vHyQ6x2xRLoBBL9wMk=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.4.0 h1:mtvR5ZXH5Ew6PSONd5lO5OXovWP1E3oAlgC8fpxor2Q=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.4.0/go.mod h1:u560+RFVfG0CBPzkXlDW43slESbBAQjgDGi3r6z+wk8=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=\ngithub.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=\ngithub.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=\ngithub.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=\ngithub.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=\ngithub.com/G-Core/gcorelabscdn-go v1.0.35 h1:7UFoL1jSb8e+JN1xxQisGE8gtflqx1vM1gH7wa9fa1E=\ngithub.com/G-Core/gcorelabscdn-go v1.0.35/go.mod h1:iSGXaTvZBzDHQW+rKFS918BgFVpONcyLEijwh8WsXpE=\ngithub.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=\ngithub.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=\ngithub.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=\ngithub.com/KscSDK/ksc-sdk-go v0.18.0 h1:Lix27hvZ9K4WTj4qUwh+2fbXYuMp9jBpVbnnmeiCg5U=\ngithub.com/KscSDK/ksc-sdk-go v0.18.0/go.mod h1:isHlJZi429ff5JLemSc10h7nznNgzJAY4MmNM8u7SBo=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=\ngithub.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw=\ngithub.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=\ngithub.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0=\ngithub.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=\ngithub.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=\ngithub.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=\ngithub.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 h1:h/33OxYLqBk0BYmEbSUy7MlvgQR/m1w1/7OJFKoPL1I=\ngithub.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0/go.mod h1:rvh3imDA6EaQi+oM/GQHkQAOHbXPKJ7EWJvfjuw141Q=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=\ngithub.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI=\ngithub.com/alibabacloud-go/alb-20200616/v2 v2.3.1 h1:IYWpYnBpRUY35vA0/Qxedqwkl2oMlwFf7UhibbUXkEE=\ngithub.com/alibabacloud-go/alb-20200616/v2 v2.3.1/go.mod h1:pUTnSOSknoHg5YtAmGrXuO+JcPlb+EYRNutf5VbW/F0=\ngithub.com/alibabacloud-go/alibabacloud-gateway-fc-util v0.0.7 h1:RDatRb9RG39HjkevgzTeiVoDDaamoB+12GHNairp3Ag=\ngithub.com/alibabacloud-go/alibabacloud-gateway-fc-util v0.0.7/go.mod h1:H0RPHXHP/ICfEQrKzQcCqXI15jcV4zaDPCOAmh3U9O8=\ngithub.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=\ngithub.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=\ngithub.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=\ngithub.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=\ngithub.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=\ngithub.com/alibabacloud-go/apig-20240327/v6 v6.0.1 h1:LneGoh/nC1Dv39qGOrXH5py2D6kFHSkr1etNuo2dxls=\ngithub.com/alibabacloud-go/apig-20240327/v6 v6.0.1/go.mod h1:VCQaugCTmRp5E1HXWFnCdpJP+UVSFkaJBn787UpR6Qw=\ngithub.com/alibabacloud-go/cas-20200407/v4 v4.1.0 h1:JldJ1EtKHzqZMQJkZaGKz4pI6TtbKCKTXNO/v2bVJ30=\ngithub.com/alibabacloud-go/cas-20200407/v4 v4.1.0/go.mod h1:q7X8C3NE71dRxR3YLwz/NESvE5X56RI2tGTJqODe7Zs=\ngithub.com/alibabacloud-go/cdn-20180510/v9 v9.0.0 h1:HNutnXWhtfPUjlUEOfMvzqVXpQip11eqK4vSMM0o+UA=\ngithub.com/alibabacloud-go/cdn-20180510/v9 v9.0.0/go.mod h1:6UcbZ0B2z0B1mnquRrsB0vCKwNcgBJE70y3PIn3y0Eo=\ngithub.com/alibabacloud-go/cloudapi-20160714/v5 v5.7.9 h1:lFUrf4dvUmbTkAW56fyKdNauSStUpNR4i7cFWWKu/pY=\ngithub.com/alibabacloud-go/cloudapi-20160714/v5 v5.7.9/go.mod h1:kPth3SgnjK42No8O5biqjrAeDgMd/cFGUktq8g9Vs4A=\ngithub.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=\ngithub.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=\ngithub.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=\ngithub.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=\ngithub.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=\ngithub.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=\ngithub.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5/go.mod h1:kUe8JqFmoVU7lfBauaDD5taFaW7mBI+xVsyHutYtabg=\ngithub.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=\ngithub.com/alibabacloud-go/darabonba-openapi/v2 v2.1.14/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=\ngithub.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15 h1:Mubp9hXZMTPWZK+WxrR+kKOVFp4Q/PDZrIIM7ByXI9Y=\ngithub.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=\ngithub.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=\ngithub.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=\ngithub.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=\ngithub.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=\ngithub.com/alibabacloud-go/dcdn-20180115/v4 v4.1.0 h1:3Q7qvpL2+k+7Twda0VE0MC0vfoRAxCtOl36S7vDLmjY=\ngithub.com/alibabacloud-go/dcdn-20180115/v4 v4.1.0/go.mod h1:dVyxkadBhESK7HlppUEjdaJmw6e5ZlZNwy8+BTSDcRE=\ngithub.com/alibabacloud-go/ddoscoo-20200101/v5 v5.0.1 h1:vEwgCBuQxrTaThLC4eMOko/XjAPT9WIg0t0gk+ABJiE=\ngithub.com/alibabacloud-go/ddoscoo-20200101/v5 v5.0.1/go.mod h1:oYNvOuLR67SMppvBmB9Hb9jnJFDQtLLEN/Rbukbq0w0=\ngithub.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=\ngithub.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=\ngithub.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=\ngithub.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=\ngithub.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=\ngithub.com/alibabacloud-go/endpoint-util v1.1.1 h1:ZkBv2/jnghxtU0p+upSU0GGzW1VL9GQdZO3mcSUTUy8=\ngithub.com/alibabacloud-go/endpoint-util v1.1.1/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=\ngithub.com/alibabacloud-go/esa-20240910/v2 v2.48.0 h1:SdtLjxay5rzshlp56bvHFSqWuKwi+rkhCfla4cRuDVU=\ngithub.com/alibabacloud-go/esa-20240910/v2 v2.48.0/go.mod h1:uSzaHIUBmr4WoixyRnc8uEuzSqxy/HQ4F8iu4RAzvHQ=\ngithub.com/alibabacloud-go/fc-20230330/v4 v4.6.8 h1:nM/hqf/9ERwN24z00kE66TQfq2NmaCUzFPUnGrwZGdY=\ngithub.com/alibabacloud-go/fc-20230330/v4 v4.6.8/go.mod h1:EQNGiZWcKvBqs6rHHyAtWau1qeTR5A/yiuUI84b7NdA=\ngithub.com/alibabacloud-go/fc-open-20210406/v2 v2.0.12 h1:A3D8Mp6qf8DfR6Dt5MpS8aDVaWfS4N85T5CvGUvgrjM=\ngithub.com/alibabacloud-go/fc-open-20210406/v2 v2.0.12/go.mod h1:F5c0E5UB3k8v6neTtw3FBcJ1YCNFzVoL1JPRHTe33u4=\ngithub.com/alibabacloud-go/ga-20191120/v4 v4.0.0 h1:bigMbQy6TXKMwhsRHqqjo+6dQcv0SZ+nzfxd8N2D7SE=\ngithub.com/alibabacloud-go/ga-20191120/v4 v4.0.0/go.mod h1:07e+SHN7j6s6hL/dSK6TZHIqvWBc0tbWW/iW5BPjM2Q=\ngithub.com/alibabacloud-go/live-20161101/v2 v2.6.0 h1:wi9/Mi5CDYeXquB39B8Ch0/CtuCDoSuQxDw1bY+dl0U=\ngithub.com/alibabacloud-go/live-20161101/v2 v2.6.0/go.mod h1:1BN//Z4vOkdEplf0pWcpF1GuIqaPJOwYuPCShljY+nI=\ngithub.com/alibabacloud-go/nlb-20220430/v4 v4.1.2 h1:dKDAynkCI9qGAlZIaNNGbiLTCLhD5yzqjlQHdbe0lNQ=\ngithub.com/alibabacloud-go/nlb-20220430/v4 v4.1.2/go.mod h1:jYaLW+5IteqlZ8becBP51zPQp42PxjMHLbqMpU5Cyds=\ngithub.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=\ngithub.com/alibabacloud-go/openapi-util v0.1.1 h1:ujGErJjG8ncRW6XtBBMphzHTvCxn4DjrVw4m04HsS28=\ngithub.com/alibabacloud-go/openapi-util v0.1.1/go.mod h1:/UehBSE2cf1gYT43GV4E+RxTdLRzURImCYY0aRmlXpw=\ngithub.com/alibabacloud-go/slb-20140515/v4 v4.0.13 h1:MtQUoGTgFqGTebY4lzFTFVsIV7QXeVN13oMzJYqvtYQ=\ngithub.com/alibabacloud-go/slb-20140515/v4 v4.0.13/go.mod h1:gWZrz3AD+izASfHjpxTOIJ8N0KMRjbIRzRZr1koy7tA=\ngithub.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=\ngithub.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=\ngithub.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=\ngithub.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=\ngithub.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=\ngithub.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=\ngithub.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA=\ngithub.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=\ngithub.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=\ngithub.com/alibabacloud-go/tea v1.4.0 h1:MSKhu/kWLPX7mplWMngki8nNt+CyUZ+kfkzaR5VpMhA=\ngithub.com/alibabacloud-go/tea v1.4.0/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=\ngithub.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=\ngithub.com/alibabacloud-go/tea-utils/v2 v2.0.4/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ=\ngithub.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=\ngithub.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=\ngithub.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=\ngithub.com/alibabacloud-go/tea-utils/v2 v2.0.9 h1:y6pUIlhjxbZl9ObDAcmA1H3c21eaAxADHTDQmBnAIgA=\ngithub.com/alibabacloud-go/tea-utils/v2 v2.0.9/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=\ngithub.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=\ngithub.com/alibabacloud-go/vod-20170321/v4 v4.11.1 h1:EPenvECObhGH01jaChRb7NRzNrk7eU3iFyZBksQS+zc=\ngithub.com/alibabacloud-go/vod-20170321/v4 v4.11.1/go.mod h1:2NX/9lVaKpd1+1GEV5zUAzQFfK9pF8Wkx81ugAnHYiw=\ngithub.com/alibabacloud-go/waf-openapi-20211001/v7 v7.5.0 h1:QYKzVRu0C/stONFvxnwbYUbpSSauMQrdReekQw4ULqk=\ngithub.com/alibabacloud-go/waf-openapi-20211001/v7 v7.5.0/go.mod h1:g+049bOg+Vh40ckFRzg5kCc7r3kJxYi5aqH/uuRZ+qA=\ngithub.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.4.0 h1:gfxyMc5g9TJ4TO/PQ8PvkGfYpDUHZnVGP0/7iTgI0Ks=\ngithub.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.4.0/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M=\ngithub.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=\ngithub.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=\ngithub.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=\ngithub.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=\ngithub.com/aliyun/credentials-go v1.4.7 h1:T17dLqEtPUFvjDRRb5giVvLh6dFT8IcNFJJb7MeyCxw=\ngithub.com/aliyun/credentials-go v1.4.7/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=\ngithub.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=\ngithub.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=\ngithub.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=\ngithub.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=\ngithub.com/aws/aws-sdk-go v1.25.3/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=\ngithub.com/aws/aws-sdk-go v1.40.45 h1:QN1nsY27ssD/JmW4s83qmSb+uL6DG4GmCDzjmJB4xUI=\ngithub.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=\ngithub.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=\ngithub.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=\ngithub.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=\ngithub.com/aws/aws-sdk-go-v2/service/acm v1.37.20 h1:lK39/l75lJkopS7WIk8bhGnWstTOfFVYtozVW8uoqlM=\ngithub.com/aws/aws-sdk-go-v2/service/acm v1.37.20/go.mod h1:3iaG4YcV+H0ERcefngFFs+ZpFfUaUY8Q0GA8TmkDtE8=\ngithub.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.1 h1:fwkGr0AyYMq/oxzBrNWVLcmSgSWVyGtFAanNs+ECRes=\ngithub.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.1/go.mod h1:PAegJVxp+CkgKZBZVEaTWBN2bHwH24FLl5sIIHYuzOU=\ngithub.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=\ngithub.com/aws/aws-sdk-go-v2/service/iam v1.53.3 h1:boKZv8dNdHznhAA68hb/dqFz5pxoWmRAOJr9LtscVCI=\ngithub.com/aws/aws-sdk-go-v2/service/iam v1.53.3/go.mod h1:E0QHh3aEwxYb7xshjvxYDELiOda7KBYJ77e/TvGhpcM=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=\ngithub.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 h1:1jIdwWOulae7bBLIgB36OZ0DINACb1wxM6wdGlx4eHE=\ngithub.com/aws/aws-sdk-go-v2/service/route53 v1.62.1/go.mod h1:tE2zGlMIlxWv+7Otap7ctRp3qeKqtnja7DZguj3Vu/Y=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=\ngithub.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=\ngithub.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=\ngithub.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=\ngithub.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=\ngithub.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=\ngithub.com/baidubce/bce-sdk-go v0.9.260 h1:1v1+2GTP+NGK3L24rJ+bnoiTaDaIy2YoaUM+ot2GTcw=\ngithub.com/baidubce/bce-sdk-go v0.9.260/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=\ngithub.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=\ngithub.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=\ngithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=\ngithub.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=\ngithub.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=\ngithub.com/byteplus-sdk/byteplus-sdk-golang v1.0.62 h1:36+wcU891+eaanXqlBSacckSyHmyy11iSFoEFVS6x/8=\ngithub.com/byteplus-sdk/byteplus-sdk-golang v1.0.62/go.mod h1:CIL/T2dxgbIA79os+wl0Fq0vCbADTZNIddV6PNYB6DY=\ngithub.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=\ngithub.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=\ngithub.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=\ngithub.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=\ngithub.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=\ngithub.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=\ngithub.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=\ngithub.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=\ngithub.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=\ngithub.com/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8=\ngithub.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k=\ngithub.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=\ngithub.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=\ngithub.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=\ngithub.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=\ngithub.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=\ngithub.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=\ngithub.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=\ngithub.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=\ngithub.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=\ngithub.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=\ngithub.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo=\ngithub.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=\ngithub.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=\ngithub.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=\ngithub.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=\ngithub.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=\ngithub.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw=\ngithub.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk=\ngithub.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-acme/alidns-20150109/v4 v4.7.0 h1:PqJ/wR0JTpL4v0Owu1uM7bPQ1Yww0eQLAuuSdLjjQaQ=\ngithub.com/go-acme/alidns-20150109/v4 v4.7.0/go.mod h1:btQvB6xZoN6ykKB74cPhiR+uvhrEE2AFVXm6RDmCHm0=\ngithub.com/go-acme/esa-20240910/v2 v2.48.0 h1:muSDyhjDTejxUGe3FTthCPCqRaEdYY9cG3N/AmU52Lc=\ngithub.com/go-acme/esa-20240910/v2 v2.48.0/go.mod h1:shPb6hzc1rJL15IJBY8HQ4GZk4E8RC52+52twutEwIg=\ngithub.com/go-acme/jdcloud-sdk-go v1.64.0 h1:AW9j5khk8tRYbpBJPxKmqdwIqgLs2Fz3HUK3hn2YXjs=\ngithub.com/go-acme/jdcloud-sdk-go v1.64.0/go.mod h1:qc/m8HNX1Zgd7GAv2DSEinup8fwy3Ted3/VVx7LB5bU=\ngithub.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=\ngithub.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=\ngithub.com/go-acme/tencentclouddnspod v1.3.24 h1:uCSiOW1EJttcnOON+MVVyVDJguFL/Q4NIGkq1CrT9p8=\ngithub.com/go-acme/tencentclouddnspod v1.3.24/go.mod h1:RKcB2wSoZncjBA0OEFj59s1ko1XDy+ZsAtk+9uMxUF0=\ngithub.com/go-acme/tencentedgdeone v1.3.38 h1:5YsVl0H4A+cwtiUqR1eZbKFdr4OWfYp2KYJopifzKyQ=\ngithub.com/go-acme/tencentedgdeone v1.3.38/go.mod h1:yyjTKVmGpMtFv5HqGODqehHnZJ4KWAbG6dAiwWDgCDY=\ngithub.com/go-cmd/cmd v1.4.3 h1:6y3G+3UqPerXvPcXvj+5QNPHT02BUw7p6PsqRxLNA7Y=\ngithub.com/go-cmd/cmd v1.4.3/go.mod h1:u3hxg/ry+D5kwh8WvUkHLAMe2zQCaXd00t35WfQaOFk=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=\ngithub.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=\ngithub.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=\ngithub.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=\ngithub.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=\ngithub.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=\ngithub.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=\ngithub.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=\ngithub.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=\ngithub.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=\ngithub.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=\ngithub.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=\ngithub.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=\ngithub.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=\ngithub.com/go-playground/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk=\ngithub.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=\ngithub.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=\ngithub.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=\ngithub.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=\ngithub.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=\ngithub.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=\ngithub.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=\ngithub.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=\ngithub.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=\ngithub.com/goccy/go-yaml v1.9.8 h1:5gMyLUeU1/6zl+WFfR1hN7D2kf+1/eRGa7DFtToiBvQ=\ngithub.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=\ngithub.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=\ngithub.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=\ngithub.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=\ngithub.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=\ngithub.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=\ngithub.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=\ngithub.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=\ngithub.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=\ngithub.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=\ngithub.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=\ngithub.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=\ngithub.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=\ngithub.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=\ngithub.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=\ngithub.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=\ngithub.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=\ngithub.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=\ngithub.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=\ngithub.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=\ngithub.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=\ngithub.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=\ngithub.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=\ngithub.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=\ngithub.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187 h1:J+U6+eUjIsBhefolFdZW5hQNJbkMj+7msxZrv56Cg2g=\ngithub.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=\ngithub.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=\ngithub.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=\ngithub.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=\ngithub.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=\ngithub.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/jdcloud-api/jdcloud-sdk-go v1.64.0 h1:xZc/ZRcrOhDx9Ra9htu6ui2gUUttmLsXIqH61LcvY4U=\ngithub.com/jdcloud-api/jdcloud-sdk-go v1.64.0/go.mod h1:UrKjuULIWLjHFlG6aSPunArE5QX57LftMmStAZJBEX8=\ngithub.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=\ngithub.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=\ngithub.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=\ngithub.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=\ngithub.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=\ngithub.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\ngithub.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=\ngithub.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=\ngithub.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=\ngithub.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=\ngithub.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=\ngithub.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=\ngithub.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=\ngithub.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=\ngithub.com/kong/go-kong v0.72.1 h1:rQ69f3Wd0Fvc3JANkavo34vePqR4uZG/YQ2y5U7d2Po=\ngithub.com/kong/go-kong v0.72.1/go.mod h1:J0vGB3wsZ2i99zly1zTRe3v7rOKpkhQZRwbcTFP76qM=\ngithub.com/kong/semver/v4 v4.0.1 h1:DIcNR8W3gfx0KabFBADPalxxsp+q/5COwIFkkhrFQ2Y=\ngithub.com/kong/semver/v4 v4.0.1/go.mod h1:LImQ0oT15pJvSns/hs2laLca2zcYoHu5EsSNY0J6/QA=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=\ngithub.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k=\ngithub.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8=\ngithub.com/luthermonson/go-proxmox v0.3.2 h1:/zUg6FCl9cAABx0xU3OIgtDtClY0gVXxOCsrceDNylc=\ngithub.com/luthermonson/go-proxmox v0.3.2/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=\ngithub.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=\ngithub.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=\ngithub.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=\ngithub.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=\ngithub.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=\ngithub.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=\ngithub.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=\ngithub.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=\ngithub.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=\ngithub.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=\ngithub.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=\ngithub.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=\ngithub.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=\ngithub.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=\ngithub.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=\ngithub.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=\ngithub.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=\ngithub.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=\ngithub.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=\ngithub.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=\ngithub.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=\ngithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/mohuatech/mohuacloud-go-sdk v0.0.0-20251115182757-6fba4d0a4c47 h1:ymaxpfg8BH3Jlecq943X/+QWOBuMp1qmRUCK+SCoN+c=\ngithub.com/mohuatech/mohuacloud-go-sdk v0.0.0-20251115182757-6fba4d0a4c47/go.mod h1:+GS72hJwcVILclv1ghdmowvKX+iT9gS42bhYLw9hcQg=\ngithub.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/namedotcom/go/v4 v4.0.2 h1:4gNkPaPRG/2tqFNUUof7jAVsA6vDutFutEOd7ivnDwA=\ngithub.com/namedotcom/go/v4 v4.0.2/go.mod h1:J6sVueHMb0qbarPgdhrzEVhEaYp+R1SCaTGl2s6/J1Q=\ngithub.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q=\ngithub.com/nats-io/jwt/v2 v2.0.3/go.mod h1:VRP+deawSXyhNjXmxPCHskrR6Mq50BqpEI5SEcNiGlY=\ngithub.com/nats-io/nats-server/v2 v2.5.0/go.mod h1:Kj86UtrXAL6LwYRA6H4RqzkHhK0Vcv2ZnKD5WbQ1t3g=\ngithub.com/nats-io/nats.go v1.12.1/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=\ngithub.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s=\ngithub.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=\ngithub.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=\ngithub.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=\ngithub.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=\ngithub.com/nrdcg/bunny-go v0.1.0 h1:GAHTRpHaG/TxfLZlqoJ8OJFzw8rI74+jOTkzxWh0uHA=\ngithub.com/nrdcg/bunny-go v0.1.0/go.mod h1:u+C9dgsspgtWVaAz6QkyV17s9fxD8viwwKoxb9XMz1A=\ngithub.com/nrdcg/desec v0.11.1 h1:ilpKmCr4gGsLcyq3RHfHNmlRzm9fzT2XbWxoVaUCS0s=\ngithub.com/nrdcg/desec v0.11.1/go.mod h1:2LuxHlOcwML/7cntu0eimONmA1U+ZxFDAonoSXr4igQ=\ngithub.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=\ngithub.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=\ngithub.com/nrdcg/namesilo v0.5.0 h1:6QNxT/XxE+f5B+7QlfWorthNzOzcGlBLRQxqi6YeBrE=\ngithub.com/nrdcg/namesilo v0.5.0/go.mod h1:4UkwlwQfDt74kSGmhLaDylnBrD94IfflnpoEaj6T2qw=\ngithub.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=\ngithub.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=\ngithub.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=\ngithub.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=\ngithub.com/onsi/ginkgo v1.16.2 h1:HFB2fbVIlhIfCfOW81bZFbiC/RvnpXSdhbF2/DJr134=\ngithub.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=\ngithub.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=\ngithub.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=\ngithub.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=\ngithub.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=\ngithub.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=\ngithub.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=\ngithub.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=\ngithub.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=\ngithub.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=\ngithub.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=\ngithub.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 h1:2nosf3P75OZv2/ZO/9Px5ZgZ5gbKrzA3joN1QMfOGMQ=\ngithub.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0/go.mod h1:lAVhWwbNaveeJmxrxuSTxMgKpF6DjnuVpn6T8WiBwYQ=\ngithub.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM=\ngithub.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c=\ngithub.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc=\ngithub.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=\ngithub.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=\ngithub.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=\ngithub.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=\ngithub.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=\ngithub.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=\ngithub.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=\ngithub.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=\ngithub.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=\ngithub.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=\ngithub.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pocketbase/dbx v1.12.0 h1:/oLErM+A0b4xI0PWTGPqSDVjzix48PqI/bng2l0PzoA=\ngithub.com/pocketbase/dbx v1.12.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=\ngithub.com/pocketbase/pocketbase v0.36.5 h1:QQhfBtOvKN4VXTwh8is5TLxMOKhPXaUcM/RKAlZ31n0=\ngithub.com/pocketbase/pocketbase v0.36.5/go.mod h1:m3tkFYh/+m6yiWHv5ED8gJczVefkbTzrlZOtsNa+bA4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=\ngithub.com/povsister/scp v0.0.0-20250701154629-777cf82de5df h1:zEgSHrxo8f6hGG1xCaqunfBq8hlfDmFd1JM0QXiQi7o=\ngithub.com/povsister/scp v0.0.0-20250701154629-777cf82de5df/go.mod h1:CiJNEeV6v0tUCNul/+gTjl+FgjfImoiuptJB9AEzqjE=\ngithub.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=\ngithub.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=\ngithub.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=\ngithub.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=\ngithub.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=\ngithub.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=\ngithub.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=\ngithub.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/qiniu/dyn v1.3.0 h1:s+xPTeV0H8yikgM4ZMBc7Rrefam8UNI3asBlkaOQg5o=\ngithub.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=\ngithub.com/qiniu/go-sdk/v7 v7.25.6 h1:89KQX16Bv2x7MxhwpzWGGvQBOPIlGpAcnPQyfS3tRok=\ngithub.com/qiniu/go-sdk/v7 v7.25.6/go.mod h1:dmKtJ2ahhPWFVi9o1D5GemmWoh/ctuB9peqTowyTO8o=\ngithub.com/qiniu/x v1.10.5 h1:7V/CYWEmo9axJULvrJN6sMYh2FdY+esN5h8jwDkA4b0=\ngithub.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=\ngithub.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=\ngithub.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=\ngithub.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=\ngithub.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=\ngithub.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=\ngithub.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=\ngithub.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=\ngithub.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.3.36 h1:zYKBmpT6l7k37LBncd4a1Qj1RvxYFAPf+I6NP5DRBOk=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.3.36/go.mod h1:zTuaHstR5s1J+qxKh4gbQldbKkaZXefxjWUV+bn01T4=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.3.45 h1:pVHnFf2G6Bw2POiX+JrO1yVCFJAPJZ3hL2xqTtnOdRQ=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.3.45/go.mod h1:mW2Ak0kGPxFjzsXArhQaYTZbIIVb1iMw/EaZ3SfPZMg=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.24/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.29/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.34/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.36/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.38/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.42/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.45/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.46/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48 h1:bCs+z6dxRaHWm/C1D/XkSOcCZ0+W2+/6HmIXjpAj+fY=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap v1.3.34 h1:/QJeztyMC2tYPJceIoObx7LZqqgFcdDM0SQ/Wd0RtEI=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap v1.3.34/go.mod h1:LiTqyLKs+CUdXeiTezJrsMcgi1RhVQ2gFuCcDxQBK9U=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.3.45 h1:yKIsmuQPgopARN20hGyOwPS059X8wVJEQjnxmpvZc70=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.3.45/go.mod h1:TCp0N1HLhVkaQfnQ+0HZRChEIsu4hKTzYs/ISVb3cbU=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf v1.3.29 h1:MxY5dIlW9e48lqyMc9xtPCmO0RlJJ+RgZMqs6yYte9w=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf v1.3.29/go.mod h1:ERH6Ek8rbThvxvqoC91U6ae+qJyUGrXPjv8sw881hho=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.3.42 h1:tzs/LQUXA/RcKP/37WQzL0EXFfWayfx3IESNEgOQmZY=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.3.42/go.mod h1:+OiMLoEYiI3UnjZbf0XBdhLn8chpAupH7/zevjXBFug=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.3.45 h1:7Hw9bVpwApnPuC6GwPb2HO1Mk+lxVqZMjI8n4IK+xRg=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.3.45/go.mod h1:m9gQ6S01nvnzPkeIWmLPWbNI2AiXIfBKQIBY+wwUUeg=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod v1.3.46 h1:tSCbSFCPgWUXsmmZb9j9ZTGkaH8IpRQCRA5bEMh2CqI=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod v1.3.46/go.mod h1:HAFMznaORzlFmPFo4kK2+pQ+gdZB3r6ZheBC76jYegE=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf v1.3.46 h1:eG0Tfqw3XCj0Tfw7/3HsBLApBjHkBKWzrB3XMfquxwM=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf v1.3.46/go.mod h1:V2tffbp8V/mW3fsBgoBmKg55oOim6zymhvfizJsWZlE=\ngithub.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=\ngithub.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=\ngithub.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=\ngithub.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=\ngithub.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=\ngithub.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=\ngithub.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=\ngithub.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=\ngithub.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=\ngithub.com/ucloud/ucloud-sdk-go v0.22.59 h1:9wPpKn5kAnG87QS8oiLjtbyS+oSRPKCzA3JmjUa687c=\ngithub.com/ucloud/ucloud-sdk-go v0.22.59/go.mod h1:dyLmFHmUfgb4RZKYQP9IArlvQ2pxzFthfhwxRzOEPIw=\ngithub.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=\ngithub.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=\ngithub.com/volcengine/ve-tos-golang-sdk/v2 v2.9.1 h1:32HAAl4KowauWe2Qf8JTQI+WcnMNPQ5tCMkUv5e+FY0=\ngithub.com/volcengine/ve-tos-golang-sdk/v2 v2.9.1/go.mod h1:IrjK84IJJTuOZOTMv/P18Ydjy/x+ow7fF7q11jAxXLM=\ngithub.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU=\ngithub.com/volcengine/volc-sdk-golang v1.0.237 h1:hpLKiS2BwDcSBtZWSz034foCbd0h3FrHTKlUMqHIdc4=\ngithub.com/volcengine/volc-sdk-golang v1.0.237/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM=\ngithub.com/volcengine/volcengine-go-sdk v1.2.15 h1:duhofGY6gVqcMUfvfa2JTo4uvfixH9rASDlJs4TwQJk=\ngithub.com/volcengine/volcengine-go-sdk v1.2.15/go.mod h1:oxoVo+A17kvkwPkIeIHPVLjSw7EQAm+l/Vau1YGHN+A=\ngithub.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8=\ngithub.com/vultr/govultr/v3 v3.27.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=\ngithub.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=\ngithub.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngithub.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=\ngithub.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=\ngithub.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=\ngithub.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=\ngithub.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=\ngithub.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=\ngithub.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=\ngithub.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=\ngo.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=\ngo.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=\ngo.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=\ngo.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM=\ngo.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=\ngo.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=\ngo.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=\ngo.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngo.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=\ngo.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=\ngo.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=\ngolang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=\ngolang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=\ngolang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=\ngolang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=\ngolang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=\ngolang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=\ngolang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=\ngolang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=\ngolang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=\ngolang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=\ngolang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=\ngolang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=\ngolang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=\ngolang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=\ngolang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=\ngolang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=\ngolang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=\ngolang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=\ngolang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=\ngolang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=\ngolang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=\ngolang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=\ngolang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=\ngolang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=\ngolang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=\ngolang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=\ngolang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=\ngolang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=\ngolang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=\ngolang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=\ngolang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=\ngolang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=\ngolang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngolang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=\ngolang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=\ngolang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=\ngolang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=\ngonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=\ngonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=\ngonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=\ngopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=\ngopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=\ngopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=\ngopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=\ngopkg.in/ns1/ns1-go.v2 v2.17.2 h1:x8YKHqCJWkC/hddfUhw7FRqTG0x3fr/0ZnWYN+i4THs=\ngopkg.in/ns1/ns1-go.v2 v2.17.2/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nk8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=\nk8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=\nk8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=\nk8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=\nk8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=\nk8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=\nk8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=\nk8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=\nk8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=\nk8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=\nk8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=\nk8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=\nmodernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=\nmodernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=\nmodernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=\nmodernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=\nmodernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=\nmodernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=\nmodernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=\nmodernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=\nmodernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=\nmodernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=\nmodernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=\nmodernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=\nmodernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=\nmodernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=\nmodernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=\nmodernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=\nmodernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=\nmodernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=\nmodernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=\nmodernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=\nmodernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=\nmodernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=\nmodernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=\nmodernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=\nmodernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=\nmodernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=\nmodernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=\nmodernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=\nmodernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=\nsigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=\nsigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=\nsigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=\nsigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=\nsigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=\nsigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=\nsigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=\nsigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=\nsoftware.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0=\nsoftware.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=\n"
  },
  {
    "path": "internal/app/app.go",
    "content": "﻿package app\n\nconst (\n\tAppName      = \"Certimate\"\n\tAppVersion   = \"0.4.19\"\n\tAppUserAgent = AppName + \"/\" + AppVersion\n)\n"
  },
  {
    "path": "internal/app/scheduler.go",
    "content": "package app\n\nimport (\n\t\"sync\"\n\t\"time\"\n\t_ \"time/tzdata\"\n\n\t\"github.com/pocketbase/pocketbase/tools/cron\"\n)\n\nvar scheduler *cron.Cron\n\nvar schedulerOnce sync.Once\n\nfunc GetScheduler() *cron.Cron {\n\tscheduler = GetApp().Cron()\n\tschedulerOnce.Do(func() {\n\t\tlocation, err := time.LoadLocation(\"Local\")\n\t\tif err == nil {\n\t\t\tscheduler.Stop()\n\t\t\tscheduler.SetTimezone(location)\n\t\t\tscheduler.Start()\n\t\t}\n\t})\n\n\treturn scheduler\n}\n"
  },
  {
    "path": "internal/app/singleton.go",
    "content": "package app\n\nimport (\n\t\"log/slog\"\n\t\"sync\"\n\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\nvar (\n\tinstance    core.App\n\tintanceOnce sync.Once\n)\n\nfunc GetApp() core.App {\n\tintanceOnce.Do(func() {\n\t\tpb := pocketbase.NewWithConfig(pocketbase.Config{\n\t\t\tHideStartBanner: true,\n\t\t})\n\n\t\tpb.RootCmd.Flags().MarkHidden(\"encryptionEnv\")\n\t\tpb.RootCmd.Flags().MarkHidden(\"queryTimeout\")\n\n\t\tpb.OnBootstrap().BindFunc(func(e *core.BootstrapEvent) error {\n\t\t\terr := e.Next()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tsettings := pb.Settings()\n\t\t\tif !settings.Batch.Enabled {\n\t\t\t\tsettings.Batch.Enabled = true\n\t\t\t\tsettings.Batch.MaxRequests = 1000\n\t\t\t\tsettings.Batch.Timeout = 30\n\t\t\t\tif err := pb.Save(settings); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\t\tinstance = pb\n\t})\n\n\treturn instance\n}\n\nfunc GetDB() dbx.Builder {\n\treturn GetApp().DB()\n}\n\nfunc GetLogger() *slog.Logger {\n\tapp := GetApp()\n\tif !app.IsBootstrapped() {\n\t\tpanic(\"MUST NOT USE THIS BEFORE APP BOOTSTRAPPED!\")\n\t}\n\n\treturn app.Logger()\n}\n"
  },
  {
    "path": "internal/certacme/account.go",
    "content": "package certacme\n\nimport (\n\t\"context\"\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/go-acme/lego/v4/lego\"\n\t\"github.com/go-acme/lego/v4/registration\"\n\t\"golang.org/x/sync/singleflight\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/internal/repository\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\nvar registrationSg singleflight.Group\n\ntype ACMEAccount = domain.ACMEAccount\n\nfunc NewACMEAccount(config *ACMEConfig, email string, register bool) (*ACMEAccount, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the acme config is nil\")\n\t}\n\tif email == \"\" {\n\t\treturn nil, errors.New(\"the email is empty\")\n\t}\n\n\tctx := context.Background()\n\taccountRepo := repository.NewACMEAccountRepository()\n\taccount, err := accountRepo.GetByCAAndEmail(ctx, string(config.CAProvider), config.CADirUrl, email)\n\tif err != nil {\n\t\tif !domain.IsRecordNotFoundError(err) {\n\t\t\treturn nil, fmt.Errorf(\"failed to get acme account record: %w\", err)\n\t\t}\n\t}\n\n\t// register new acme account if not exists\n\tif account == nil {\n\t\tif !register {\n\t\t\treturn nil, errors.New(\"the acme account does not exist\")\n\t\t}\n\n\t\tkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tkeyPEM, err := xcert.ConvertECPrivateKeyToPEM(key)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\taccount = &ACMEAccount{\n\t\t\tCA:         string(config.CAProvider),\n\t\t\tEmail:      email,\n\t\t\tPrivateKey: keyPEM,\n\t\t\tACMEDirUrl: config.CADirUrl,\n\t\t}\n\t\tlegoCfg := lego.NewConfig(account)\n\t\tlegoCfg.CADirURL = config.CADirUrl\n\t\tlegoClient, err := lego.NewClient(legoCfg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tvar regres *registration.Resource\n\t\tvar regerr error\n\t\tif legoClient.GetExternalAccountRequired() {\n\t\t\tif config.EABKid == \"\" {\n\t\t\t\treturn nil, errors.New(\"missing or invalid eab kid\")\n\t\t\t}\n\t\t\tif config.EABHmacKey == \"\" {\n\t\t\t\treturn nil, errors.New(\"missing or invalid eab hmac key\")\n\t\t\t}\n\n\t\t\t// patch, see https://github.com/go-acme/lego/issues/2634\n\t\t\tkeyId := strings.TrimSpace(config.EABKid)\n\t\t\tkeyEncoded := strings.TrimSpace(config.EABHmacKey)\n\t\t\tkeyEncoded = strings.ReplaceAll(strings.ReplaceAll(keyEncoded, \"+\", \"-\"), \"/\", \"_\")\n\t\t\tkeyEncoded = strings.TrimRight(keyEncoded, \"=\")\n\n\t\t\tregres, regerr = legoClient.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{\n\t\t\t\tTermsOfServiceAgreed: true,\n\t\t\t\tKid:                  keyId,\n\t\t\t\tHmacEncoded:          keyEncoded,\n\t\t\t})\n\t\t} else {\n\t\t\tregres, regerr = legoClient.Registration.Register(registration.RegisterOptions{\n\t\t\t\tTermsOfServiceAgreed: true,\n\t\t\t})\n\t\t}\n\t\tif regerr != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to register acme account: %w\", regerr)\n\t\t}\n\n\t\taccount.ACMEAccount = &regres.Body\n\t\taccount.ACMEAcctUrl = regres.URI\n\n\t\tif _, err := accountRepo.Save(ctx, account); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to save acme account record: %w\", err)\n\t\t}\n\t}\n\n\treturn account, nil\n}\n\nfunc NewACMEAccountWithSingleFlight(config *ACMEConfig, email string) (*ACMEAccount, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the acme config is nil\")\n\t}\n\tif email == \"\" {\n\t\treturn nil, errors.New(\"the email is empty\")\n\t}\n\n\tresp, err, _ := registrationSg.Do(fmt.Sprintf(\"%s|%s|%s\", string(config.CAProvider), config.CADirUrl, email), func() (any, error) {\n\t\treturn NewACMEAccount(config, email, true)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn resp.(*ACMEAccount), nil\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/registry.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ProviderFactoryFunc func(options *ProviderFactoryOptions) (certifier.ACMEChallenger, error)\n\ntype ProviderFactoryOptions struct {\n\tProviderAccessConfig   map[string]any\n\tProviderExtendedConfig map[string]any\n\tDnsPropagationTimeout  int\n\tDnsTTL                 int\n}\n\ntype Registry[T comparable] interface {\n\tRegister(T, ProviderFactoryFunc) error\n\tRegisterAlias(T, T) error\n\tMustRegister(T, ProviderFactoryFunc)\n\tMustRegisterAlias(T, T)\n\tGet(T) (ProviderFactoryFunc, error)\n}\n\ntype registry[T comparable] struct {\n\tfactories map[T]ProviderFactoryFunc\n}\n\nfunc (r *registry[T]) Register(name T, factory ProviderFactoryFunc) error {\n\tif _, exists := r.factories[name]; exists {\n\t\treturn fmt.Errorf(\"provider '%v' already registered\", name)\n\t}\n\n\tr.factories[name] = factory\n\treturn nil\n}\n\nfunc (r *registry[T]) RegisterAlias(name T, alias T) error {\n\tfactory, err := r.Get(alias)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = r.Register(name, factory)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (r *registry[T]) MustRegister(name T, factory ProviderFactoryFunc) {\n\tif err := r.Register(name, factory); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc (r *registry[T]) MustRegisterAlias(name T, alias T) {\n\tif err := r.RegisterAlias(name, alias); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc (r *registry[T]) Get(name T) (ProviderFactoryFunc, error) {\n\tif factory, exists := r.factories[name]; exists {\n\t\treturn factory, nil\n\t}\n\n\treturn nil, fmt.Errorf(\"provider '%v' not registered\", name)\n}\n\nfunc newRegistry[T comparable]() Registry[T] {\n\treturn &registry[T]{factories: make(map[T]ProviderFactoryFunc)}\n}\n\nvar (\n\tACMEDns01Registries  = newRegistry[domain.ACMEDns01ProviderType]()\n\tACMEHttp01Registries = newRegistry[domain.ACMEHttp01ProviderType]()\n)\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_35cn.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\twest35cn \"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/35cn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderType35cn, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigFor35cn{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := west35cn.NewChallenger(&west35cn.ChallengerConfig{\n\t\t\tUsername:              credentials.Username,\n\t\t\tApiPassword:           credentials.ApiPassword,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_51dnscom.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\tdnscom \"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/51dnscom\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderType51DNScom, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigFor51DNScom{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := dnscom.NewChallenger(&dnscom.ChallengerConfig{\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tApiSecret:             credentials.ApiSecret,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_acmedns.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/acmedns\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeACMEDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForACMEDNS{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := acmedns.NewChallenger(&acmedns.ChallengerConfig{\n\t\t\tServerUrl:   credentials.ServerUrl,\n\t\t\tCredentials: credentials.Credentials,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_acmehttpreq.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/acmehttpreq\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeACMEHttpReq, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForACMEHttpReq{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := acmehttpreq.NewChallenger(&acmehttpreq.ChallengerConfig{\n\t\t\tEndpoint:              credentials.Endpoint,\n\t\t\tMode:                  credentials.Mode,\n\t\t\tUsername:              credentials.Username,\n\t\t\tPassword:              credentials.Password,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_akamai_edgedns.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\takamaiedgedns \"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/akamai-edgedns\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeAkamaiEdgeDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAkamai{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := akamaiedgedns.NewChallenger(&akamaiedgedns.ChallengerConfig{\n\t\t\tHost:                  credentials.Host,\n\t\t\tClientToken:           credentials.ClientToken,\n\t\t\tClientSecret:          credentials.ClientSecret,\n\t\t\tAccessToken:           credentials.AccessToken,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n\n\tACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeAkamai, domain.ACMEDns01ProviderTypeAkamaiEdgeDNS)\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_aliyun_dns.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/aliyun\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeAliyunDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyun.NewChallenger(&aliyun.ChallengerConfig{\n\t\t\tAccessKeyId:           credentials.AccessKeyId,\n\t\t\tAccessKeySecret:       credentials.AccessKeySecret,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n\n\tACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeAliyun, domain.ACMEDns01ProviderTypeAliyunDNS)\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_aliyun_esa.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\taliyunesa \"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/aliyun-esa\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeAliyunESA, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyunesa.NewChallenger(&aliyunesa.ChallengerConfig{\n\t\t\tAccessKeyId:           credentials.AccessKeyId,\n\t\t\tAccessKeySecret:       credentials.AccessKeySecret,\n\t\t\tRegion:                xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_arvancloud.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/arvancloud\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeArvanCloud, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForArvanCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := arvancloud.NewChallenger(&arvancloud.ChallengerConfig{\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_aws_route53.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\tawsroute53 \"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/aws-route53\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeAWSRoute53, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAWS{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := awsroute53.NewChallenger(&awsroute53.ChallengerConfig{\n\t\t\tAccessKeyId:           credentials.AccessKeyId,\n\t\t\tSecretAccessKey:       credentials.SecretAccessKey,\n\t\t\tRegion:                xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tHostedZoneId:          xmaps.GetString(options.ProviderExtendedConfig, \"hostedZoneId\"),\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n\n\tACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeAWS, domain.ACMEDns01ProviderTypeAWSRoute53)\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_azure_dns.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\tazuredns \"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/azure-dns\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeAzureDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAzure{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := azuredns.NewChallenger(&azuredns.ChallengerConfig{\n\t\t\tTenantId:              credentials.TenantId,\n\t\t\tClientId:              credentials.ClientId,\n\t\t\tClientSecret:          credentials.ClientSecret,\n\t\t\tSubscriptionId:        credentials.SubscriptionId,\n\t\t\tResourceGroupName:     credentials.ResourceGroupName,\n\t\t\tCloudName:             credentials.CloudName,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n\n\tACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeAzure, domain.ACMEDns01ProviderTypeAzureDNS)\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_baiducloud_dns.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/baiducloud\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeBaiduCloudDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForBaiduCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := baiducloud.NewChallenger(&baiducloud.ChallengerConfig{\n\t\t\tAccessKeyId:           credentials.AccessKeyId,\n\t\t\tSecretAccessKey:       credentials.SecretAccessKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n\n\tACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeBaiduCloud, domain.ACMEDns01ProviderTypeBaiduCloudDNS)\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_bookmyname.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/bookmyname\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeBookMyName, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForBookMyName{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := bookmyname.NewChallenger(&bookmyname.ChallengerConfig{\n\t\t\tUsername:              credentials.Username,\n\t\t\tPassword:              credentials.Password,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_bunny.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/bunny\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeBunny, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForBunny{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := bunny.NewChallenger(&bunny.ChallengerConfig{\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_cloudflare.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/cloudflare\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeCloudflare, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForCloudflare{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := cloudflare.NewChallenger(&cloudflare.ChallengerConfig{\n\t\t\tDnsApiToken:           credentials.DnsApiToken,\n\t\t\tZoneApiToken:          credentials.ZoneApiToken,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_cloudns.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/cloudns\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeClouDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForClouDNS{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := cloudns.NewChallenger(&cloudns.ChallengerConfig{\n\t\t\tAuthId:                credentials.AuthId,\n\t\t\tAuthPassword:          credentials.AuthPassword,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_cmcccloud_dns.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/cmcccloud\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeCMCCCloudDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForCMCCCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := cmcccloud.NewChallenger(&cmcccloud.ChallengerConfig{\n\t\t\tAccessKeyId:           credentials.AccessKeyId,\n\t\t\tAccessKeySecret:       credentials.AccessKeySecret,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n\n\tACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeCMCCCloud, domain.ACMEDns01ProviderTypeCMCCCloudDNS)\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_constellix.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\tconstellix \"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/constellix\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeConstellix, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForConstellix{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := constellix.NewChallenger(&constellix.ChallengerConfig{\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tSecretKey:             credentials.SecretKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_cpanel.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/cpanel\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeCPanel, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForCPanel{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := cpanel.NewChallenger(&cpanel.ChallengerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tUsername:                 credentials.Username,\n\t\t\tApiToken:                 credentials.ApiToken,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tDnsPropagationTimeout:    options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                   options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_ctcccloud_smartdns.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/ctcccloud\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeCTCCCloudSmartDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForCTCCCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ctcccloud.NewChallenger(&ctcccloud.ChallengerConfig{\n\t\t\tAccessKeyId:           credentials.AccessKeyId,\n\t\t\tSecretAccessKey:       credentials.SecretAccessKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n\n\tACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeCTCCCloud, domain.ACMEDns01ProviderTypeCTCCCloudSmartDNS)\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_desec.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/desec\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDeSEC, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForDeSEC{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := desec.NewChallenger(&desec.ChallengerConfig{\n\t\t\tToken:                 credentials.Token,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_digitalocean.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\tdigitalocean \"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/digitalocean\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDigitalOcean, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForDigitalOcean{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := digitalocean.NewChallenger(&digitalocean.ChallengerConfig{\n\t\t\tAccessToken:           credentials.AccessToken,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_dnsexit.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dnsexit\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDNSExit, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForDNSExit{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := dnsexit.NewChallenger(&dnsexit.ChallengerConfig{\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_dnsla.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dnsla\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDNSLA, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForDNSLA{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := dnsla.NewChallenger(&dnsla.ChallengerConfig{\n\t\t\tApiId:                 credentials.ApiId,\n\t\t\tApiSecret:             credentials.ApiSecret,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_dnsmadeeasy.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dnsmadeeasy\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDNSMadeEasy, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForDNSMadeEasy{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := dnsmadeeasy.NewChallenger(&dnsmadeeasy.ChallengerConfig{\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tApiSecret:             credentials.ApiSecret,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_duckdns.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\tduckdns \"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/duckdns\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDuckDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForDuckDNS{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := duckdns.NewChallenger(&duckdns.ChallengerConfig{\n\t\t\tToken:                 credentials.Token,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_dynu.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dynu\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDynu, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForDynu{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := dynu.NewChallenger(&dynu.ChallengerConfig{\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_dynv6.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dynv6\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDynv6, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForDynv6{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := dynv6.NewChallenger(&dynv6.ChallengerConfig{\n\t\t\tHttpToken:             credentials.HttpToken,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_gandinet.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/gandinet\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeGandinet, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForGandinet{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := gandinet.NewChallenger(&gandinet.ChallengerConfig{\n\t\t\tPersonalAccessToken:   credentials.PersonalAccessToken,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_gcore.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/gcore\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeGcore, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForGcore{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := gcore.NewChallenger(&gcore.ChallengerConfig{\n\t\t\tApiToken:              credentials.ApiToken,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_gname.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/gname\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeGname, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForGname{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := gname.NewChallenger(&gname.ChallengerConfig{\n\t\t\tAppId:                 credentials.AppId,\n\t\t\tAppKey:                credentials.AppKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_godaddy.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/godaddy\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeGoDaddy, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForGoDaddy{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := godaddy.NewChallenger(&godaddy.ChallengerConfig{\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tApiSecret:             credentials.ApiSecret,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_hetzner.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/hetzner\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeHetzner, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForHetzner{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := hetzner.NewChallenger(&hetzner.ChallengerConfig{\n\t\t\tApiToken:              credentials.ApiToken,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_hostingde.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/hostingde\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeHostingde, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForHostingde{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := hostingde.NewChallenger(&hostingde.ChallengerConfig{\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_hostinger.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/hostinger\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeHostinger, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForHostinger{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := hostinger.NewChallenger(&hostinger.ChallengerConfig{\n\t\t\tApiToken:              credentials.ApiToken,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_huaweicloud_dns.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/huaweicloud\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeHuaweiCloudDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForHuaweiCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := huaweicloud.NewChallenger(&huaweicloud.ChallengerConfig{\n\t\t\tAccessKeyId:           credentials.AccessKeyId,\n\t\t\tSecretAccessKey:       credentials.SecretAccessKey,\n\t\t\tRegion:                xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n\n\tACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeHuaweiCloud, domain.ACMEDns01ProviderTypeHuaweiCloudDNS)\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_infomaniak.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/infomaniak\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeInfomaniak, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForInfomaniak{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := infomaniak.NewChallenger(&infomaniak.ChallengerConfig{\n\t\t\tAccessToken:           credentials.AccessToken,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_ionos.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/ionos\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeIONOS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForIONOS{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ionos.NewChallenger(&ionos.ChallengerConfig{\n\t\t\tApiKeyPublicPrefix:    credentials.ApiKeyPublicPrefix,\n\t\t\tApiKeySecret:          credentials.ApiKeySecret,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_jdcloud_dns.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/jdcloud\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeJDCloudDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForJDCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := jdcloud.NewChallenger(&jdcloud.ChallengerConfig{\n\t\t\tAccessKeyId:           credentials.AccessKeyId,\n\t\t\tAccessKeySecret:       credentials.AccessKeySecret,\n\t\t\tRegionId:              xmaps.GetString(options.ProviderExtendedConfig, \"regionId\"),\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n\n\tACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeJDCloud, domain.ACMEDns01ProviderTypeJDCloudDNS)\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_linode.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/linode\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeLinode, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForLinode{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := linode.NewChallenger(&linode.ChallengerConfig{\n\t\t\tAccessToken:           credentials.AccessToken,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_local.go",
    "content": "package certifiers\n\nimport (\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/http01/local\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEHttp01Registries.MustRegister(domain.ACMEHttp01ProviderTypeLocal, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tprovider, err := local.NewChallenger(&local.ChallengerConfig{\n\t\t\tWebRootPath: xmaps.GetString(options.ProviderExtendedConfig, \"webRootPath\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_namecheap.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\tnamecheap \"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/namecheap\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeNamecheap, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForNamecheap{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := namecheap.NewChallenger(&namecheap.ChallengerConfig{\n\t\t\tUsername:              credentials.Username,\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_namedotcom.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/namedotcom\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeNameDotCom, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForNameDotCom{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := namedotcom.NewChallenger(&namedotcom.ChallengerConfig{\n\t\t\tUsername:              credentials.Username,\n\t\t\tApiToken:              credentials.ApiToken,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_namesilo.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/namesilo\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeNameSilo, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForNameSilo{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := namesilo.NewChallenger(&namesilo.ChallengerConfig{\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_netcup.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/netcup\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeNetcup, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForNetcup{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := netcup.NewChallenger(&netcup.ChallengerConfig{\n\t\t\tCustomerNumber:        credentials.CustomerNumber,\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tApiPassword:           credentials.ApiPassword,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_netlify.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\tnetlify \"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/netlify\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeNetlify, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForNetlify{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := netlify.NewChallenger(&netlify.ChallengerConfig{\n\t\t\tApiToken:              credentials.ApiToken,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_ns1.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/ns1\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeNS1, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForNS1{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ns1.NewChallenger(&ns1.ChallengerConfig{\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_ovhcloud.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/ovhcloud\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeOVHcloud, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForOVHcloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ovhcloud.NewChallenger(&ovhcloud.ChallengerConfig{\n\t\t\tEndpoint:              credentials.Endpoint,\n\t\t\tAuthMethod:            credentials.AuthMethod,\n\t\t\tApplicationKey:        credentials.ApplicationKey,\n\t\t\tApplicationSecret:     credentials.ApplicationSecret,\n\t\t\tConsumerKey:           credentials.ConsumerKey,\n\t\t\tClientId:              credentials.ClientId,\n\t\t\tClientSecret:          credentials.ClientSecret,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_porkbun.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/porkbun\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypePorkbun, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForPorkbun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := porkbun.NewChallenger(&porkbun.ChallengerConfig{\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tSecretApiKey:          credentials.SecretApiKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_powerdns.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/powerdns\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypePowerDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForPowerDNS{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := powerdns.NewChallenger(&powerdns.ChallengerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiKey:                   credentials.ApiKey,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tDnsPropagationTimeout:    options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                   options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_qingcloud_dns.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/qingcloud\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeQingCloudDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForQingCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := qingcloud.NewChallenger(&qingcloud.ChallengerConfig{\n\t\t\tAccessKeyId:           credentials.AccessKeyId,\n\t\t\tSecretAccessKey:       credentials.SecretAccessKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n\n\tACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeQingCloud, domain.ACMEDns01ProviderTypeQingCloudDNS)\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_rainyun.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/rainyun\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeRainYun, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForRainYun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := rainyun.NewChallenger(&rainyun.ChallengerConfig{\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_rfc2136.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/rfc2136\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeRFC2136, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForRFC2136{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := rfc2136.NewChallenger(&rfc2136.ChallengerConfig{\n\t\t\tHost:                  credentials.Host,\n\t\t\tPort:                  credentials.Port,\n\t\t\tTsigAlgorithm:         credentials.TsigAlgorithm,\n\t\t\tTsigKey:               credentials.TsigKey,\n\t\t\tTsigSecret:            credentials.TsigSecret,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_s3.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/http01/s3\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEHttp01Registries.MustRegister(domain.ACMEHttp01ProviderTypeS3, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForS3{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := s3.NewChallenger(&s3.ChallengerConfig{\n\t\t\tEndpoint:                 credentials.Endpoint,\n\t\t\tAccessKey:                credentials.AccessKey,\n\t\t\tSecretKey:                credentials.SecretKey,\n\t\t\tSignatureVersion:         credentials.SignatureVersion,\n\t\t\tUsePathStyle:             credentials.UsePathStyle,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tRegion:                   xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tBucket:                   xmaps.GetString(options.ProviderExtendedConfig, \"bucket\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_spaceship.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/spaceship\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeSpaceship, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForSpaceship{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := spaceship.NewChallenger(&spaceship.ChallengerConfig{\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tApiSecret:             credentials.ApiSecret,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_ssh.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/http01/ssh\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEHttp01Registries.MustRegister(domain.ACMEHttp01ProviderTypeSSH, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForSSH{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tjumpServers := make([]ssh.ServerConfig, len(credentials.JumpServers))\n\t\tfor i, jumpServer := range credentials.JumpServers {\n\t\t\tjumpServers[i] = ssh.ServerConfig{\n\t\t\t\tSshHost:          jumpServer.Host,\n\t\t\t\tSshPort:          jumpServer.Port,\n\t\t\t\tSshAuthMethod:    jumpServer.AuthMethod,\n\t\t\t\tSshUsername:      jumpServer.Username,\n\t\t\t\tSshPassword:      jumpServer.Password,\n\t\t\t\tSshKey:           jumpServer.Key,\n\t\t\t\tSshKeyPassphrase: jumpServer.KeyPassphrase,\n\t\t\t}\n\t\t}\n\n\t\tprovider, err := ssh.NewChallenger(&ssh.ChallengerConfig{\n\t\t\tServerConfig: ssh.ServerConfig{\n\t\t\t\tSshHost:          credentials.Host,\n\t\t\t\tSshPort:          credentials.Port,\n\t\t\t\tSshAuthMethod:    credentials.AuthMethod,\n\t\t\t\tSshUsername:      credentials.Username,\n\t\t\t\tSshPassword:      credentials.Password,\n\t\t\t\tSshKey:           credentials.Key,\n\t\t\t\tSshKeyPassphrase: credentials.KeyPassphrase,\n\t\t\t},\n\t\t\tJumpServers: jumpServers,\n\t\t\tUseSCP:      xmaps.GetBool(options.ProviderExtendedConfig, \"useSCP\"),\n\t\t\tWebRootPath: xmaps.GetString(options.ProviderExtendedConfig, \"webRootPath\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_technitiumdns.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/technitiumdns\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeTechnitiumDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTechnitiumDNS{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := technitiumdns.NewChallenger(&technitiumdns.ChallengerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiToken:                 credentials.ApiToken,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tDnsPropagationTimeout:    options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                   options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_tencentcloud_dns.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/tencentcloud\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeTencentCloudDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTencentCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := tencentcloud.NewChallenger(&tencentcloud.ChallengerConfig{\n\t\t\tSecretId:              credentials.SecretId,\n\t\t\tSecretKey:             credentials.SecretKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n\n\tACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeTencentCloud, domain.ACMEDns01ProviderTypeTencentCloudDNS)\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_tencentcloud_eo.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\tteo \"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/tencentcloud-eo\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeTencentCloudEO, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTencentCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := teo.NewChallenger(&teo.ChallengerConfig{\n\t\t\tSecretId:              credentials.SecretId,\n\t\t\tSecretKey:             credentials.SecretKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_todaynic.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/todaynic\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeTodayNIC, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTodayNIC{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := todaynic.NewChallenger(&todaynic.ChallengerConfig{\n\t\t\tUserId:                credentials.UserId,\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_ucloud_udnr.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/ucloud\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeUCloudUDNR, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForUCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ucloud.NewChallenger(&ucloud.ChallengerConfig{\n\t\t\tPrivateKey:            credentials.PrivateKey,\n\t\t\tPublicKey:             credentials.PublicKey,\n\t\t\tProjectId:             credentials.ProjectId,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n\n\tACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeUCloud, domain.ACMEDns01ProviderTypeUCloudUDNR)\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_vercel.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/vercel\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeVercel, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForVercel{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := vercel.NewChallenger(&vercel.ChallengerConfig{\n\t\t\tApiAccessToken:        credentials.ApiAccessToken,\n\t\t\tTeamId:                credentials.TeamId,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_volcengine_dns.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/volcengine\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeVolcEngineDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForVolcEngine{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := volcengine.NewChallenger(&volcengine.ChallengerConfig{\n\t\t\tAccessKeyId:           credentials.AccessKeyId,\n\t\t\tSecretAccessKey:       credentials.SecretAccessKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n\n\tACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeVolcEngine, domain.ACMEDns01ProviderTypeVolcEngineDNS)\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_vultr.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/vultr\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeVultr, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForVultr{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := vultr.NewChallenger(&vultr.ChallengerConfig{\n\t\t\tApiKey:                credentials.ApiKey,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_westcn.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/westcn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeWestcn, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForWestcn{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := westcn.NewChallenger(&westcn.ChallengerConfig{\n\t\t\tUsername:              credentials.Username,\n\t\t\tApiPassword:           credentials.ApiPassword,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/certifiers/sp_xinnet.go",
    "content": "package certifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\txinnet \"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/xinnet\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeXinnet, func(options *ProviderFactoryOptions) (challenge.Provider, error) {\n\t\tcredentials := domain.AccessConfigForXinnet{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := xinnet.NewChallenger(&xinnet.ChallengerConfig{\n\t\t\tAgentId:               credentials.AgentId,\n\t\t\tApiPassword:           credentials.ApiPassword,\n\t\t\tDnsPropagationTimeout: options.DnsPropagationTimeout,\n\t\t\tDnsTTL:                options.DnsTTL,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certacme/client.go",
    "content": "package certacme\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/internal/repository\"\n\t\"github.com/go-acme/lego/v4/lego\"\n)\n\ntype ACMEClient struct {\n\tclient  *lego.Client\n\taccount *ACMEAccount\n}\n\nfunc NewACMEClient(config *ACMEConfig, email string, configures ...func(*lego.Config) error) (*ACMEClient, error) {\n\taccount, err := NewACMEAccountWithSingleFlight(config, email)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmergedConfigures := []func(*lego.Config) error{\n\t\tfunc(legoCfg *lego.Config) error {\n\t\t\tlegoCfg.CADirURL = config.CADirUrl\n\t\t\tlegoCfg.Certificate.KeyType = config.CertifierKeyType\n\t\t\treturn nil\n\t\t},\n\t}\n\tmergedConfigures = append(mergedConfigures, configures...)\n\treturn newACMEClientWithAccount(account, mergedConfigures...)\n}\n\nfunc NewACMEClientWithAccount(account *ACMEAccount, configures ...func(*lego.Config) error) (*ACMEClient, error) {\n\treturn newACMEClientWithAccount(account, configures...)\n}\n\nfunc newACMEClientWithAccount(account *ACMEAccount, configures ...func(*lego.Config) error) (*ACMEClient, error) {\n\tif account == nil {\n\t\treturn nil, errors.New(\"the acme account is nil\")\n\t}\n\n\tlegoCfg := lego.NewConfig(account)\n\tlegoCfg.CADirURL = account.ACMEDirUrl\n\n\tsettingsRepo := repository.NewSettingsRepository()\n\tsettings, _ := settingsRepo.GetByName(context.Background(), domain.SettingsNameSSLProvider)\n\tif settings != nil {\n\t\tsslProviderSettings := settings.Content.AsSSLProvider()\n\t\tif sslProviderSettings.Timeout > 0 {\n\t\t\tlegoCfg.Certificate.Timeout = time.Duration(sslProviderSettings.Timeout) * time.Second\n\t\t}\n\t}\n\n\terrs := make([]error, 0)\n\tfor _, configure := range configures {\n\t\tif err := configure(legoCfg); err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn nil, errors.Join(errs...)\n\t}\n\n\tlegoClient, err := lego.NewClient(legoCfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &ACMEClient{\n\t\tclient:  legoClient,\n\t\taccount: account,\n\t}, nil\n}\n"
  },
  {
    "path": "internal/certacme/client_obtain.go",
    "content": "package certacme\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\t\"github.com/go-acme/lego/v4/certificate\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/challenge/http01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/internal/certacme/certifiers\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\ntype ObtainCertificateRequest struct {\n\tDomainOrIPs       []string\n\tPrivateKeyType    certcrypto.KeyType\n\tPrivateKeyPEM     string\n\tValidityNotBefore time.Time\n\tValidityNotAfter  time.Time\n\tNoCommonName      bool\n\n\t// 提供商相关\n\tChallengeType          string\n\tProvider               string\n\tProviderAccessConfig   map[string]any\n\tProviderExtendedConfig map[string]any\n\n\t// 解析相关\n\tDisableFollowCNAME bool\n\tNameservers        []string\n\n\t// DNS-01 质询相关\n\tDnsPropagationWait    int\n\tDnsPropagationTimeout int\n\tDnsTTL                int\n\n\t// HTTP-01 质询相关\n\tHttpDelayWait int\n\n\t// ACME 相关\n\tPreferredChain string\n\tACMEProfile    string\n\n\t// ARI 相关\n\tARIReplacesAcctUrl string\n\tARIReplacesCertId  string\n}\n\ntype ObtainCertificateResponse struct {\n\tCSR                  string\n\tFullChainCertificate string\n\tIssuerCertificate    string\n\tPrivateKey           string\n\tACMEAcctUrl          string\n\tACMECertUrl          string\n\tARIReplaced          bool\n}\n\nfunc (c *ACMEClient) ObtainCertificate(ctx context.Context, request *ObtainCertificateRequest) (*ObtainCertificateResponse, error) {\n\ttype result struct {\n\t\tres *ObtainCertificateResponse\n\t\terr error\n\t}\n\n\tdone := make(chan result, 1)\n\n\tgo func() {\n\t\tres, err := c.sendObtainCertificateRequest(request)\n\t\tdone <- result{res, err}\n\t}()\n\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn nil, ctx.Err()\n\tcase r := <-done:\n\t\treturn r.res, r.err\n\t}\n}\n\nfunc (c *ACMEClient) sendObtainCertificateRequest(request *ObtainCertificateRequest) (*ObtainCertificateResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"the request is nil\")\n\t}\n\n\tos.Setenv(\"LEGO_DISABLE_CNAME_SUPPORT\", strconv.FormatBool(request.DisableFollowCNAME))\n\n\tswitch request.ChallengeType {\n\tcase \"dns-01\":\n\t\t{\n\t\t\tproviderFactory, err := certifiers.ACMEDns01Registries.Get(domain.ACMEDns01ProviderType(request.Provider))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tprovider, err := providerFactory(&certifiers.ProviderFactoryOptions{\n\t\t\t\tProviderAccessConfig:   request.ProviderAccessConfig,\n\t\t\t\tProviderExtendedConfig: request.ProviderExtendedConfig,\n\t\t\t\tDnsPropagationTimeout:  request.DnsPropagationTimeout,\n\t\t\t\tDnsTTL:                 request.DnsTTL,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to initialize dns-01 provider '%s': %w\", request.Provider, err)\n\t\t\t}\n\n\t\t\tc.client.Challenge.SetDNS01Provider(provider,\n\t\t\t\tdns01.CondOption(\n\t\t\t\t\tlen(request.Nameservers) > 0,\n\t\t\t\t\tdns01.AddRecursiveNameservers(dns01.ParseNameservers(request.Nameservers)),\n\t\t\t\t),\n\t\t\t\tdns01.CondOption(\n\t\t\t\t\trequest.DnsPropagationWait > 0,\n\t\t\t\t\tdns01.PropagationWait(time.Duration(request.DnsPropagationWait)*time.Second, true),\n\t\t\t\t),\n\t\t\t\tdns01.CondOption(\n\t\t\t\t\tlen(request.Nameservers) > 0 || request.DnsPropagationWait > 0,\n\t\t\t\t\tdns01.DisableAuthoritativeNssPropagationRequirement(),\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\n\tcase \"http-01\":\n\t\t{\n\t\t\tproviderFactory, err := certifiers.ACMEHttp01Registries.Get(domain.ACMEHttp01ProviderType(request.Provider))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tprovider, err := providerFactory(&certifiers.ProviderFactoryOptions{\n\t\t\t\tProviderAccessConfig:   request.ProviderAccessConfig,\n\t\t\t\tProviderExtendedConfig: request.ProviderExtendedConfig,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to initialize http-01 provider '%s': %w\", request.Provider, err)\n\t\t\t}\n\n\t\t\tc.client.Challenge.SetHTTP01Provider(provider,\n\t\t\t\thttp01.SetDelay(time.Duration(request.HttpDelayWait)*time.Second),\n\t\t\t)\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported challenge type: '%s'\", request.ChallengeType)\n\t}\n\n\tvar privkey crypto.PrivateKey\n\tif request.PrivateKeyPEM != \"\" {\n\t\tpk, err := certcrypto.ParsePEMPrivateKey([]byte(request.PrivateKeyPEM))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse private key: %w\", err)\n\t\t}\n\n\t\tprivkey = pk\n\t}\n\n\treq := certificate.ObtainRequest{\n\t\tDomains:        request.DomainOrIPs,\n\t\tPrivateKey:     privkey,\n\t\tBundle:         true,\n\t\tPreferredChain: request.PreferredChain,\n\t\tProfile:        request.ACMEProfile,\n\t\tNotBefore:      request.ValidityNotBefore,\n\t\tNotAfter:       request.ValidityNotAfter,\n\t\tReplacesCertID: lo.If(request.ARIReplacesAcctUrl == c.account.ACMEAcctUrl, request.ARIReplacesCertId).Else(\"\"),\n\t}\n\tresp, err := c.client.Certificate.Obtain(req)\n\tif err != nil {\n\t\tariErr := &acme.AlreadyReplacedError{}\n\t\tif !errors.As(err, &ariErr) {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tlog.Warnf(\"the certificate has already been replaced, try to obtain again without ARI ...\")\n\n\t\t// reset ARI and retry if failure\n\t\treq.ReplacesCertID = \"\"\n\t\tresp, err = c.client.Certificate.Obtain(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &ObtainCertificateResponse{\n\t\tCSR:                  strings.TrimSpace(string(resp.CSR)),\n\t\tFullChainCertificate: strings.TrimSpace(string(resp.Certificate)),\n\t\tIssuerCertificate:    strings.TrimSpace(string(resp.IssuerCertificate)),\n\t\tPrivateKey:           strings.TrimSpace(string(resp.PrivateKey)),\n\t\tACMEAcctUrl:          c.account.ACMEAcctUrl,\n\t\tACMECertUrl:          resp.CertURL,\n\t\tARIReplaced:          req.ReplacesCertID != \"\",\n\t}, nil\n}\n"
  },
  {
    "path": "internal/certacme/client_revoke.go",
    "content": "package certacme\n\nimport (\n\t\"context\"\n\t\"errors\"\n)\n\ntype RevokeCertificateRequest struct {\n\tCertificate string\n}\n\ntype RevokeCertificateResponse struct{}\n\nfunc (c *ACMEClient) RevokeCertificate(ctx context.Context, request *RevokeCertificateRequest) (*RevokeCertificateResponse, error) {\n\ttype result struct {\n\t\tres *RevokeCertificateResponse\n\t\terr error\n\t}\n\n\tdone := make(chan result, 1)\n\n\tgo func() {\n\t\tres, err := c.sendRevokeCertificateRequest(request)\n\t\tdone <- result{res, err}\n\t}()\n\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn nil, ctx.Err()\n\tcase r := <-done:\n\t\treturn r.res, r.err\n\t}\n}\n\nfunc (c *ACMEClient) sendRevokeCertificateRequest(request *RevokeCertificateRequest) (*RevokeCertificateResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"the request is nil\")\n\t}\n\n\terr := c.client.Certificate.Revoke([]byte(request.Certificate))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &RevokeCertificateResponse{}, nil\n}\n"
  },
  {
    "path": "internal/certacme/config.go",
    "content": "package certacme\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"strings\"\n\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/internal/repository\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nvar acmeDirUrls = map[string]string{\n\tstring(domain.CAProviderTypeLetsEncrypt):         \"https://acme-v02.api.letsencrypt.org/directory\",\n\tstring(domain.CAProviderTypeLetsEncryptStaging):  \"https://acme-staging-v02.api.letsencrypt.org/directory\",\n\tstring(domain.CAProviderTypeActalisSSL):          \"https://acme-api.actalis.com/acme/directory\",\n\tstring(domain.CAProviderTypeDigiCert):            \"https://acme.digicert.com/v2/acme/directory\",\n\tstring(domain.CAProviderTypeGlobalSignAtlas):     \"https://emea.acme.atlas.globalsign.com/directory\",\n\tstring(domain.CAProviderTypeGoogleTrustServices): \"https://dv.acme-v02.api.pki.goog/directory\",\n\tstring(domain.CAProviderTypeLiteSSL):             \"https://acme.litessl.com/acme/v2/directory\",\n\tstring(domain.CAProviderTypeSSLCom):              \"https://acme.ssl.com/sslcom-dv-rsa\",\n\tstring(domain.CAProviderTypeSSLCom) + \"RSA\":      \"https://acme.ssl.com/sslcom-dv-rsa\",\n\tstring(domain.CAProviderTypeSSLCom) + \"ECC\":      \"https://acme.ssl.com/sslcom-dv-ecc\",\n\tstring(domain.CAProviderTypeSectigo):             \"https://acme.sectigo.com/v2/DV\",\n\tstring(domain.CAProviderTypeSectigo) + \"DV\":      \"https://acme.sectigo.com/v2/DV\",\n\tstring(domain.CAProviderTypeSectigo) + \"OV\":      \"https://acme.sectigo.com/v2/OV\",\n\tstring(domain.CAProviderTypeSectigo) + \"EV\":      \"https://acme.sectigo.com/v2/EV\",\n\tstring(domain.CAProviderTypeZeroSSL):             \"https://acme.zerossl.com/v2/DV90\",\n}\n\ntype ACMEConfigOptions struct {\n\tCAProvider       string\n\tCAAccessConfig   map[string]any\n\tCAProviderConfig map[string]any\n\tCertifierKeyType certcrypto.KeyType\n}\n\ntype ACMEConfig struct {\n\tCAProvider       domain.CAProviderType\n\tCADirUrl         string\n\tEABKid           string\n\tEABHmacKey       string\n\tCertifierKeyType certcrypto.KeyType\n}\n\nfunc NewACMEConfig(options *ACMEConfigOptions) (*ACMEConfig, error) {\n\tif options == nil {\n\t\treturn nil, errors.New(\"the options is nil\")\n\t}\n\n\tcaProvider := options.CAProvider\n\tcaAccessConfig := options.CAAccessConfig\n\n\tif options.CAProvider == \"\" {\n\t\tsettingsRepo := repository.NewSettingsRepository()\n\t\tsettings, _ := settingsRepo.GetByName(context.Background(), domain.SettingsNameSSLProvider)\n\t\tif settings != nil {\n\t\t\tsslProviderSettings := settings.Content.AsSSLProvider()\n\t\t\tcaProvider = string(sslProviderSettings.Provider)\n\t\t\tcaAccessConfig = sslProviderSettings.Configs[sslProviderSettings.Provider]\n\t\t}\n\t}\n\n\tif caProvider == \"\" {\n\t\t// default CA: Let's Encrypt\n\t\tcaProvider = string(domain.AccessProviderTypeLetsEncrypt)\n\t}\n\n\tif caAccessConfig == nil {\n\t\tcaAccessConfig = make(map[string]any)\n\t}\n\n\tca := &ACMEConfig{CAProvider: domain.CAProviderType(caProvider), CertifierKeyType: options.CertifierKeyType}\n\tswitch ca.CAProvider {\n\tcase domain.CAProviderTypeSectigo:\n\t\tcredentials := &domain.AccessConfigForGlobalSectigo{}\n\t\tif err := xmaps.Populate(caAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, err\n\t\t} else if strings.EqualFold(credentials.ValidationType, \"DV\") {\n\t\t\tca.CADirUrl = acmeDirUrls[string(domain.CAProviderTypeSectigo)+\"DV\"]\n\t\t} else if strings.EqualFold(credentials.ValidationType, \"OV\") {\n\t\t\tca.CADirUrl = acmeDirUrls[string(domain.CAProviderTypeSectigo)+\"OV\"]\n\t\t} else if strings.EqualFold(credentials.ValidationType, \"EV\") {\n\t\t\tca.CADirUrl = acmeDirUrls[string(domain.CAProviderTypeSectigo)+\"EV\"]\n\t\t} else {\n\t\t\tca.CADirUrl = acmeDirUrls[string(domain.CAProviderTypeSectigo)]\n\t\t}\n\n\tcase domain.CAProviderTypeSSLCom:\n\t\tif strings.HasPrefix(string(options.CertifierKeyType), \"RSA\") {\n\t\t\tca.CADirUrl = acmeDirUrls[string(domain.CAProviderTypeSSLCom)+\"RSA\"]\n\t\t} else if strings.HasPrefix(string(options.CertifierKeyType), \"EC\") {\n\t\t\tca.CADirUrl = acmeDirUrls[string(domain.CAProviderTypeSSLCom)+\"ECC\"]\n\t\t} else {\n\t\t\tca.CADirUrl = acmeDirUrls[string(domain.CAProviderTypeSSLCom)]\n\t\t}\n\n\tcase domain.CAProviderTypeACMECA:\n\t\tcredentials := &domain.AccessConfigForACMECA{}\n\t\tif err := xmaps.Populate(caAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, err\n\t\t} else if credentials.Endpoint == \"\" {\n\t\t\treturn nil, errors.New(\"the endpoint of custom ACME CA is empty\")\n\t\t}\n\t\tca.CADirUrl = credentials.Endpoint\n\n\tdefault:\n\t\tendpoint := acmeDirUrls[string(ca.CAProvider)]\n\t\tif endpoint == \"\" {\n\t\t\treturn nil, errors.New(\"the endpoint of the ACME CA provider is empty\")\n\t\t}\n\t\tca.CADirUrl = endpoint\n\t}\n\n\teab := domain.AccessConfigForACMEExternalAccountBinding{}\n\tif err := xmaps.Populate(caAccessConfig, &eab); err != nil {\n\t\treturn nil, err\n\t}\n\tca.EABKid = eab.EabKid\n\tca.EABHmacKey = eab.EabHmacKey\n\n\treturn ca, nil\n}\n"
  },
  {
    "path": "internal/certacme/logging.go",
    "content": "package certacme\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"log/slog\"\n\t\"os\"\n\t\"strings\"\n\n\tlegolog \"github.com/go-acme/lego/v4/log\"\n)\n\ntype legoLogger struct {\n\tcallLogger *slog.Logger\n\tlegoLogger legolog.StdLogger\n}\n\nfunc (l *legoLogger) Fatal(args ...any) {\n\tl.callLogger.Error(\"go-acme/lego: \" + fmt.Sprint(args...))\n\tl.legoLogger.Fatal(args...)\n}\n\nfunc (l *legoLogger) Fatalln(args ...any) {\n\tl.Fatal(fmt.Sprintln(args...))\n}\n\nfunc (l *legoLogger) Fatalf(format string, args ...any) {\n\tl.Fatal(fmt.Sprintf(format, args...))\n}\n\nfunc (l *legoLogger) Print(args ...any) {\n\tmessage := fmt.Sprint(args...)\n\tprint := l.callLogger.Debug\n\tif strings.HasPrefix(message, \"[WARN] \") {\n\t\tmessage = strings.TrimPrefix(message, \"[WARN] \")\n\t\tprint = l.callLogger.Warn\n\t} else if strings.HasPrefix(message, \"[INFO] \") {\n\t\tmessage = strings.TrimPrefix(message, \"[INFO] \")\n\t\tprint = l.callLogger.Info\n\t}\n\n\tprint(\"go-acme/lego: \" + message)\n\tl.legoLogger.Print(message)\n}\n\nfunc (l *legoLogger) Println(args ...any) {\n\tl.Print(fmt.Sprintln(args...))\n}\n\nfunc (l *legoLogger) Printf(format string, args ...any) {\n\tl.Print(fmt.Sprintf(format, args...))\n}\n\nfunc NewLegoLogger(logger *slog.Logger) legolog.StdLogger {\n\treturn &legoLogger{\n\t\tcallLogger: logger,\n\n\t\t// https://github.com/go-acme/lego/blob/master/log/logger.go\n\t\tlegoLogger: log.New(os.Stderr, \"\", log.LstdFlags),\n\t}\n}\n"
  },
  {
    "path": "internal/certificate/service.go",
    "content": "package certificate\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/pocketbase/dbx\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/certacme\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/internal/domain/dtos\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertificateService struct {\n\tacmeAccountRepo acmeAccountRepository\n\tcertificateRepo certificateRepository\n\tsettingsRepo    settingsRepository\n}\n\nfunc NewCertificateService(\n\tacmeAccountRepo acmeAccountRepository,\n\tcertificateRepo certificateRepository,\n\tsettingsRepo settingsRepository,\n) *CertificateService {\n\treturn &CertificateService{\n\t\tacmeAccountRepo: acmeAccountRepo,\n\t\tcertificateRepo: certificateRepo,\n\t\tsettingsRepo:    settingsRepo,\n\t}\n}\n\nfunc (s *CertificateService) InitSchedule(ctx context.Context) error {\n\tapp.GetScheduler().MustAdd(\"cleanupCertificateExpired\", \"0 0 * * *\", func() {\n\t\ts.cleanupExpiredCertificates(context.Background())\n\t})\n\n\treturn nil\n}\n\nfunc (s *CertificateService) DownloadCertificate(ctx context.Context, req *dtos.CertificateDownloadReq) (*dtos.CertificateDownloadResp, error) {\n\tcertificate, err := s.certificateRepo.GetById(ctx, req.CertificateId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcanonicalName := strings.Split(certificate.SubjectAltNames, \";\")[0]\n\tcanonicalName = strings.ReplaceAll(canonicalName, \"*\", \"_\")\n\n\tvar buf bytes.Buffer\n\tzipWriter := zip.NewWriter(&buf)\n\tdefer zipWriter.Close()\n\n\tvar bytes []byte\n\tswitch strings.ToUpper(req.CertificateFormat) {\n\tcase \"\", string(domain.CertificateFormatTypePEM):\n\t\t{\n\t\t\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certificate.Certificate)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to extract certs: %w\", err)\n\t\t\t}\n\n\t\t\tcertWriter, err := zipWriter.Create(fmt.Sprintf(\"%s.pem\", canonicalName))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t} else {\n\t\t\t\t_, err = certWriter.Write([]byte(certificate.Certificate))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tserverCertWriter, err := zipWriter.Create(fmt.Sprintf(\"%s (server).pem\", canonicalName))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t} else {\n\t\t\t\t_, err = serverCertWriter.Write([]byte(serverCertPEM))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tintermediaCertWriter, err := zipWriter.Create(fmt.Sprintf(\"%s (intermedia).pem\", canonicalName))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t} else {\n\t\t\t\t_, err = intermediaCertWriter.Write([]byte(intermediaCertPEM))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tkeyWriter, err := zipWriter.Create(fmt.Sprintf(\"%s.key\", canonicalName))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t} else {\n\t\t\t\t_, err = keyWriter.Write([]byte(certificate.PrivateKey))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terr = zipWriter.Close()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tbytes = buf.Bytes()\n\t\t}\n\n\tcase string(domain.CertificateFormatTypePFX):\n\t\t{\n\t\t\tconst pfxPassword = \"certimate\"\n\n\t\t\tcertPFX, err := xcert.TransformCertificateFromPEMToPFX(certificate.Certificate, certificate.PrivateKey, pfxPassword)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tcertWriter, err := zipWriter.Create(fmt.Sprintf(\"%s.pfx\", canonicalName))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t} else {\n\t\t\t\t_, err = certWriter.Write(certPFX)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tkeyWriter, err := zipWriter.Create(\"pfx-password.txt\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t} else {\n\t\t\t\t_, err = keyWriter.Write([]byte(pfxPassword))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terr = zipWriter.Close()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tbytes = buf.Bytes()\n\t\t}\n\n\tcase string(domain.CertificateFormatTypeJKS):\n\t\t{\n\t\t\tconst jksPassword = \"certimate\"\n\n\t\t\tcertJKS, err := xcert.TransformCertificateFromPEMToJKS(certificate.Certificate, certificate.PrivateKey, jksPassword, jksPassword, jksPassword)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tcertWriter, err := zipWriter.Create(fmt.Sprintf(\"%s.jks\", canonicalName))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t} else {\n\t\t\t\t_, err = certWriter.Write(certJKS)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tkeyWriter, err := zipWriter.Create(\"jks-password.txt\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t} else {\n\t\t\t\t_, err = keyWriter.Write([]byte(jksPassword))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terr = zipWriter.Close()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tbytes = buf.Bytes()\n\t\t}\n\n\tdefault:\n\t\treturn nil, domain.ErrInvalidParams\n\t}\n\n\tresp := &dtos.CertificateDownloadResp{\n\t\tFileFormat: \"zip\",\n\t\tFileBytes:  bytes,\n\t}\n\treturn resp, nil\n}\n\nfunc (s *CertificateService) RevokeCertificate(ctx context.Context, req *dtos.CertificateRevokeReq) (*dtos.CertificateRevokeResp, error) {\n\tcertificate, err := s.certificateRepo.GetById(ctx, req.CertificateId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif certificate.ACMEAcctUrl == \"\" || certificate.ACMECertUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"could not revoke a certificate which is not issued in Certimate\")\n\t}\n\tif certificate.IsRevoked {\n\t\treturn nil, fmt.Errorf(\"could not revoke a certificate which is already revoked\")\n\t}\n\n\tacmeAccount, err := s.acmeAccountRepo.GetByAcctUrl(ctx, certificate.ACMEAcctUrl)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to revoke certificate: could not find acme account: %w\", err)\n\t}\n\n\tlegoClient, err := certacme.NewACMEClientWithAccount(acmeAccount)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to revoke certificate: could not initialize acme config: %w\", err)\n\t}\n\n\trevokeReq := &certacme.RevokeCertificateRequest{\n\t\tCertificate: certificate.Certificate,\n\t}\n\t_, err = legoClient.RevokeCertificate(ctx, revokeReq)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to revoke certificate: %w\", err)\n\t}\n\n\tcertificate.IsRevoked = true\n\tcertificate, err = s.certificateRepo.Save(ctx, certificate)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &dtos.CertificateRevokeResp{}, nil\n}\n\nfunc (s *CertificateService) cleanupExpiredCertificates(ctx context.Context) error {\n\tsettings, err := s.settingsRepo.GetByName(ctx, domain.SettingsNamePersistence)\n\tif err != nil {\n\t\tif errors.Is(err, domain.ErrRecordNotFound) {\n\t\t\treturn nil\n\t\t}\n\n\t\tapp.GetLogger().Error(\"failed to get persistence settings\", slog.Any(\"error\", err))\n\t\treturn err\n\t}\n\n\tpersistenceSettings := settings.Content.AsPersistence()\n\tif persistenceSettings.CertificatesRetentionMaxDays != 0 {\n\t\tret, err := s.certificateRepo.DeleteWhere(\n\t\t\tcontext.Background(),\n\t\t\tdbx.NewExp(fmt.Sprintf(\"validityNotAfter<DATETIME('now', '-%d days')\", persistenceSettings.CertificatesRetentionMaxDays)),\n\t\t)\n\t\tif err != nil {\n\t\t\tapp.GetLogger().Error(\"failed to delete expired certificates\", slog.Any(\"error\", err))\n\t\t\treturn err\n\t\t}\n\n\t\tif ret > 0 {\n\t\t\tapp.GetLogger().Info(fmt.Sprintf(\"cleanup %d expired certificates\", ret))\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/certificate/service_deps.go",
    "content": "package certificate\n\nimport (\n\t\"context\"\n\n\t\"github.com/pocketbase/dbx\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\ntype acmeAccountRepository interface {\n\tGetByAcctUrl(ctx context.Context, acctUrl string) (*domain.ACMEAccount, error)\n}\n\ntype certificateRepository interface {\n\tListExpiringSoon(ctx context.Context) ([]*domain.Certificate, error)\n\tGetById(ctx context.Context, id string) (*domain.Certificate, error)\n\tSave(ctx context.Context, certificate *domain.Certificate) (*domain.Certificate, error)\n\tDeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error)\n}\n\ntype settingsRepository interface {\n\tGetByName(ctx context.Context, name string) (*domain.Settings, error)\n}\n"
  },
  {
    "path": "internal/certmgmt/client.go",
    "content": "package certmgmt\n\nimport (\n\t\"log/slog\"\n)\n\ntype Client struct {\n\tlogger *slog.Logger\n}\n\ntype ClientConfigure func(*Client)\n\nfunc NewClient(configures ...ClientConfigure) *Client {\n\tclient := &Client{}\n\tfor _, configure := range configures {\n\t\tconfigure(client)\n\t}\n\treturn client\n}\n\nfunc WithLogger(logger *slog.Logger) ClientConfigure {\n\treturn func(c *Client) {\n\t\tc.logger = logger\n\t}\n}\n"
  },
  {
    "path": "internal/certmgmt/client_deploy.go",
    "content": "package certmgmt\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/certmgmt/deployers\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\ntype DeployCertificateRequest struct {\n\t// 提供商相关\n\tProvider               string\n\tProviderAccessConfig   map[string]any\n\tProviderExtendedConfig map[string]any\n\n\t// 证书相关\n\tCertificate string\n\tPrivateKey  string\n}\n\ntype DeployCertificateResponse struct{}\n\nfunc (c *Client) DeployCertificate(ctx context.Context, request *DeployCertificateRequest) (*DeployCertificateResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"the request is nil\")\n\t}\n\n\tproviderFactory, err := deployers.Registries.Get(domain.DeploymentProviderType(request.Provider))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprovider, err := providerFactory(&deployers.ProviderFactoryOptions{\n\t\tProviderAccessConfig:   request.ProviderAccessConfig,\n\t\tProviderExtendedConfig: request.ProviderExtendedConfig,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to initialize deployment provider '%s': %w\", request.Provider, err)\n\t}\n\n\tprovider.SetLogger(c.logger)\n\tif _, err := provider.Deploy(ctx, request.Certificate, request.PrivateKey); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &DeployCertificateResponse{}, nil\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/registry.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype ProviderFactoryFunc func(options *ProviderFactoryOptions) (deployer.Provider, error)\n\ntype ProviderFactoryOptions struct {\n\tProviderAccessConfig   map[string]any\n\tProviderExtendedConfig map[string]any\n}\n\ntype Registry[T comparable] interface {\n\tRegister(T, ProviderFactoryFunc) error\n\tMustRegister(T, ProviderFactoryFunc)\n\tGet(T) (ProviderFactoryFunc, error)\n}\n\ntype registry[T comparable] struct {\n\tfactories map[T]ProviderFactoryFunc\n}\n\nfunc (r *registry[T]) Register(name T, factory ProviderFactoryFunc) error {\n\tif _, exists := r.factories[name]; exists {\n\t\treturn fmt.Errorf(\"provider '%v' already registered\", name)\n\t}\n\n\tr.factories[name] = factory\n\treturn nil\n}\n\nfunc (r *registry[T]) MustRegister(name T, factory ProviderFactoryFunc) {\n\tif err := r.Register(name, factory); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc (r *registry[T]) Get(name T) (ProviderFactoryFunc, error) {\n\tif factory, exists := r.factories[name]; exists {\n\t\treturn factory, nil\n\t}\n\n\treturn nil, fmt.Errorf(\"provider '%v' not registered\", name)\n}\n\nfunc newRegistry[T comparable]() Registry[T] {\n\treturn &registry[T]{factories: make(map[T]ProviderFactoryFunc)}\n}\n\nvar Registries = newRegistry[domain.DeploymentProviderType]()\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_1panel.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tonepanel \"github.com/certimate-go/certimate/pkg/core/deployer/providers/1panel\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderType1Panel, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigFor1Panel{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := onepanel.NewDeployer(&onepanel.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiVersion:               credentials.ApiVersion,\n\t\t\tApiKey:                   credentials.ApiKey,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tNodeName:                 xmaps.GetString(options.ProviderExtendedConfig, \"nodeName\"),\n\t\t\tResourceType:             xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tWebsiteMatchPattern:      xmaps.GetString(options.ProviderExtendedConfig, \"websiteMatchPattern\"),\n\t\t\tWebsiteId:                xmaps.GetInt64(options.ProviderExtendedConfig, \"websiteId\"),\n\t\t\tCertificateId:            xmaps.GetInt64(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_1panel_console.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\topconsole \"github.com/certimate-go/certimate/pkg/core/deployer/providers/1panel-console\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderType1PanelConsole, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigFor1Panel{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := opconsole.NewDeployer(&opconsole.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiVersion:               credentials.ApiVersion,\n\t\t\tApiKey:                   credentials.ApiKey,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tAutoRestart:              xmaps.GetBool(options.ProviderExtendedConfig, \"autoRestart\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_alb.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyunalb \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-alb\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunALB, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyunalb.NewDeployer(&aliyunalb.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.AccessKeySecret,\n\t\t\tResourceGroupId: credentials.ResourceGroupId,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tResourceType:    xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tLoadbalancerId:  xmaps.GetString(options.ProviderExtendedConfig, \"loadbalancerId\"),\n\t\t\tListenerId:      xmaps.GetString(options.ProviderExtendedConfig, \"listenerId\"),\n\t\t\tDomain:          xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_apigw.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyunapigw \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-apigw\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunAPIGW, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyunapigw.NewDeployer(&aliyunapigw.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.AccessKeySecret,\n\t\t\tResourceGroupId:    credentials.ResourceGroupId,\n\t\t\tRegion:             xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tServiceType:        xmaps.GetString(options.ProviderExtendedConfig, \"serviceType\"),\n\t\t\tGatewayId:          xmaps.GetString(options.ProviderExtendedConfig, \"gatewayId\"),\n\t\t\tGroupId:            xmaps.GetString(options.ProviderExtendedConfig, \"groupId\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_cas.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyuncas \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-cas\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunCAS, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyuncas.NewDeployer(&aliyuncas.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.AccessKeySecret,\n\t\t\tResourceGroupId: credentials.ResourceGroupId,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_casdeploy.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyuncasdeploy \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-cas-deploy\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunCASDeploy, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyuncasdeploy.NewDeployer(&aliyuncasdeploy.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.AccessKeySecret,\n\t\t\tResourceGroupId: credentials.ResourceGroupId,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tResourceIds:     lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, \"resourceIds\"), \";\"), func(s string, _ int) bool { return s != \"\" }),\n\t\t\tContactIds:      lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, \"contactIds\"), \";\"), func(s string, _ int) bool { return s != \"\" }),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_cdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyuncdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-cdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyuncdn.NewDeployer(&aliyuncdn.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.AccessKeySecret,\n\t\t\tResourceGroupId:    credentials.ResourceGroupId,\n\t\t\tRegion:             xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_clb.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyunclb \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-clb\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunCLB, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyunclb.NewDeployer(&aliyunclb.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.AccessKeySecret,\n\t\t\tResourceGroupId: credentials.ResourceGroupId,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tResourceType:    xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tLoadbalancerId:  xmaps.GetString(options.ProviderExtendedConfig, \"loadbalancerId\"),\n\t\t\tListenerPort:    xmaps.GetOrDefaultInt32(options.ProviderExtendedConfig, \"listenerPort\", 443),\n\t\t\tDomain:          xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_dcdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyundcdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-dcdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunDCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyundcdn.NewDeployer(&aliyundcdn.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.AccessKeySecret,\n\t\t\tResourceGroupId:    credentials.ResourceGroupId,\n\t\t\tRegion:             xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_ddospro.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyunddospro \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-ddospro\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunDDoSPro, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyunddospro.NewDeployer(&aliyunddospro.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.AccessKeySecret,\n\t\t\tResourceGroupId:    credentials.ResourceGroupId,\n\t\t\tRegion:             xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_esa.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyunesa \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-esa\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunESA, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyunesa.NewDeployer(&aliyunesa.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.AccessKeySecret,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tSiteId:          xmaps.GetInt64(options.ProviderExtendedConfig, \"siteId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_esasaas.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyunesasaas \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-esa-saas\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunESASaaS, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyunesasaas.NewDeployer(&aliyunesasaas.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.AccessKeySecret,\n\t\t\tRegion:             xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tSiteId:             xmaps.GetInt64(options.ProviderExtendedConfig, \"siteId\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_fc.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyunfc \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-fc\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunFC, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyunfc.NewDeployer(&aliyunfc.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.AccessKeySecret,\n\t\t\tResourceGroupId:    credentials.ResourceGroupId,\n\t\t\tRegion:             xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tServiceVersion:     xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"serviceVersion\", \"3.0\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_ga.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyunga \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-ga\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunGA, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyunga.NewDeployer(&aliyunga.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.AccessKeySecret,\n\t\t\tResourceGroupId: credentials.ResourceGroupId,\n\t\t\tResourceType:    xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tAcceleratorId:   xmaps.GetString(options.ProviderExtendedConfig, \"acceleratorId\"),\n\t\t\tListenerId:      xmaps.GetString(options.ProviderExtendedConfig, \"listenerId\"),\n\t\t\tDomain:          xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_live.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyunlive \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-live\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunLive, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyunlive.NewDeployer(&aliyunlive.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.AccessKeySecret,\n\t\t\tRegion:             xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_nlb.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyunnlb \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-nlb\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunNLB, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyunnlb.NewDeployer(&aliyunnlb.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.AccessKeySecret,\n\t\t\tResourceGroupId: credentials.ResourceGroupId,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tResourceType:    xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tLoadbalancerId:  xmaps.GetString(options.ProviderExtendedConfig, \"loadbalancerId\"),\n\t\t\tListenerId:      xmaps.GetString(options.ProviderExtendedConfig, \"listenerId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_oss.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyunoss \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-oss\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunOSS, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyunoss.NewDeployer(&aliyunoss.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.AccessKeySecret,\n\t\t\tResourceGroupId: credentials.ResourceGroupId,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tBucket:          xmaps.GetString(options.ProviderExtendedConfig, \"bucket\"),\n\t\t\tDomain:          xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_vod.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyunvod \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-vod\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunVOD, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyunvod.NewDeployer(&aliyunvod.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.AccessKeySecret,\n\t\t\tResourceGroupId:    credentials.ResourceGroupId,\n\t\t\tRegion:             xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aliyun_waf.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\taliyunwaf \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-waf\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAliyunWAF, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAliyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := aliyunwaf.NewDeployer(&aliyunwaf.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.AccessKeySecret,\n\t\t\tResourceGroupId: credentials.ResourceGroupId,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tServiceVersion:  xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"serviceVersion\", \"3.0\"),\n\t\t\tServiceType:     xmaps.GetString(options.ProviderExtendedConfig, \"serviceType\"),\n\t\t\tInstanceId:      xmaps.GetString(options.ProviderExtendedConfig, \"instanceId\"),\n\t\t\tResourceProduct: xmaps.GetString(options.ProviderExtendedConfig, \"resourceProduct\"),\n\t\t\tResourceId:      xmaps.GetString(options.ProviderExtendedConfig, \"resourceId\"),\n\t\t\tResourcePort:    xmaps.GetOrDefaultInt32(options.ProviderExtendedConfig, \"resourcePort\", 443),\n\t\t\tDomain:          xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_apisix.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/apisix\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAPISIX, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAPISIX{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := apisix.NewDeployer(&apisix.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiKey:                   credentials.ApiKey,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tResourceType:             xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tCertificateId:            xmaps.GetString(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aws_acm.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tawsacm \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aws-acm\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAWSACM, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAWS{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := awsacm.NewDeployer(&awsacm.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tSecretAccessKey: credentials.SecretAccessKey,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tCertificateArn:  xmaps.GetString(options.ProviderExtendedConfig, \"certificateArn\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aws_cloudfront.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tawscloudfront \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aws-cloudfront\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAWSCloudFront, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAWS{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := awscloudfront.NewDeployer(&awscloudfront.DeployerConfig{\n\t\t\tAccessKeyId:       credentials.AccessKeyId,\n\t\t\tSecretAccessKey:   credentials.SecretAccessKey,\n\t\t\tRegion:            xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tDistributionId:    xmaps.GetString(options.ProviderExtendedConfig, \"distributionId\"),\n\t\t\tCertificateSource: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"certificateSource\", \"ACM\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_aws_iam.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tawsiam \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aws-iam\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAWSIAM, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAWS{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := awsiam.NewDeployer(&awsiam.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tSecretAccessKey: credentials.SecretAccessKey,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tCertificatePath: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"certificatePath\", \"/\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_azure_keyvault.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tazurekeyvault \"github.com/certimate-go/certimate/pkg/core/deployer/providers/azure-keyvault\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeAzureKeyVault, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForAzure{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := azurekeyvault.NewDeployer(&azurekeyvault.DeployerConfig{\n\t\t\tTenantId:        credentials.TenantId,\n\t\t\tClientId:        credentials.ClientId,\n\t\t\tClientSecret:    credentials.ClientSecret,\n\t\t\tCloudName:       credentials.CloudName,\n\t\t\tKeyVaultName:    xmaps.GetString(options.ProviderExtendedConfig, \"keyvaultName\"),\n\t\t\tCertificateName: xmaps.GetString(options.ProviderExtendedConfig, \"certificateName\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_baiducloud_appblb.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbaiducloudappblb \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baiducloud-appblb\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeBaiduCloudAppBLB, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForBaiduCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := baiducloudappblb.NewDeployer(&baiducloudappblb.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tSecretAccessKey: credentials.SecretAccessKey,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tResourceType:    xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tLoadbalancerId:  xmaps.GetString(options.ProviderExtendedConfig, \"loadbalancerId\"),\n\t\t\tListenerPort:    xmaps.GetInt32(options.ProviderExtendedConfig, \"listenerPort\"),\n\t\t\tDomain:          xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_baiducloud_blb.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbaiducloudblb \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baiducloud-blb\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeBaiduCloudBLB, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForBaiduCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := baiducloudblb.NewDeployer(&baiducloudblb.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tSecretAccessKey: credentials.SecretAccessKey,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tResourceType:    xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tLoadbalancerId:  xmaps.GetString(options.ProviderExtendedConfig, \"loadbalancerId\"),\n\t\t\tListenerPort:    xmaps.GetInt32(options.ProviderExtendedConfig, \"listenerPort\"),\n\t\t\tDomain:          xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_baiducloud_cdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbaiducloudcdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baiducloud-cdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeBaiduCloudCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForBaiduCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := baiducloudcdn.NewDeployer(&baiducloudcdn.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tSecretAccessKey:    credentials.SecretAccessKey,\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_baiducloud_cert.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbaiducloudcert \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baiducloud-cert\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeBaiduCloudCert, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForBaiduCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := baiducloudcert.NewDeployer(&baiducloudcert.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tSecretAccessKey: credentials.SecretAccessKey,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_baishan_cdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbaishancdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baishan-cdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeBaishanCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForBaishan{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := baishancdn.NewDeployer(&baishancdn.DeployerConfig{\n\t\t\tApiToken:           credentials.ApiToken,\n\t\t\tResourceType:       xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t\tCertificateId:      xmaps.GetString(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_baotapanel.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbaotapanel \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baotapanel\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeBaotaPanel, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForBaotaPanel{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := baotapanel.NewDeployer(&baotapanel.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiKey:                   credentials.ApiKey,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tSiteType:                 xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"siteType\", \"other\"),\n\t\t\tSiteNames:                lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, \"siteNames\"), \";\"), func(s string, _ int) bool { return s != \"\" }),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_baotapanel_console.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbaotapanelconsole \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baotapanel-console\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeBaotaPanelConsole, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForBaotaPanel{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := baotapanelconsole.NewDeployer(&baotapanelconsole.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiKey:                   credentials.ApiKey,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tAutoRestart:              xmaps.GetBool(options.ProviderExtendedConfig, \"autoRestart\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_baotapanelgo.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbaotapanelgo \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baotapanelgo\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeBaotaPanelGo, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForBaotaPanelGo{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := baotapanelgo.NewDeployer(&baotapanelgo.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiKey:                   credentials.ApiKey,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tSiteType:                 xmaps.GetString(options.ProviderExtendedConfig, \"siteType\"),\n\t\t\tSiteNames:                lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, \"siteNames\"), \";\"), func(s string, _ int) bool { return s != \"\" }),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_baotapanelgo_console.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbaotapanelgoconsole \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baotapanelgo-console\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeBaotaPanelGoConsole, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForBaotaPanelGo{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := baotapanelgoconsole.NewDeployer(&baotapanelgoconsole.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiKey:                   credentials.ApiKey,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_baotawaf.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbaotawaf \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baotawaf\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeBaotaWAF, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForBaotaWAF{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := baotawaf.NewDeployer(&baotawaf.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiKey:                   credentials.ApiKey,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tSiteNames:                lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, \"siteNames\"), \";\"), func(s string, _ int) bool { return s != \"\" }),\n\t\t\tSitePort:                 xmaps.GetOrDefaultInt32(options.ProviderExtendedConfig, \"sitePort\", 443),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_baotawaf_console.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbaotawafconsole \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baotawaf-console\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeBaotaWAFConsole, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForBaotaWAF{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := baotawafconsole.NewDeployer(&baotawafconsole.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiKey:                   credentials.ApiKey,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_bunny_cdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbunnycdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/bunny-cdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeBunnyCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForBunny{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := bunnycdn.NewDeployer(&bunnycdn.DeployerConfig{\n\t\t\tApiKey:     credentials.ApiKey,\n\t\t\tPullZoneId: xmaps.GetString(options.ProviderExtendedConfig, \"pullZoneId\"),\n\t\t\tHostname:   xmaps.GetString(options.ProviderExtendedConfig, \"hostname\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_byteplus_cdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbytepluscdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/byteplus-cdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeBytePlusCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForBytePlus{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := bytepluscdn.NewDeployer(&bytepluscdn.DeployerConfig{\n\t\t\tAccessKey:          credentials.AccessKey,\n\t\t\tSecretKey:          credentials.SecretKey,\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_cachefly.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/cachefly\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeCacheFly, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForCacheFly{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := cachefly.NewDeployer(&cachefly.DeployerConfig{\n\t\t\tApiToken: credentials.ApiToken,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_cdnfly.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/cdnfly\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeCdnfly, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForCdnfly{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tdeployer, err := cdnfly.NewDeployer(&cdnfly.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiKey:                   credentials.ApiKey,\n\t\t\tApiSecret:                credentials.ApiSecret,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tResourceType:             xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tSiteId:                   xmaps.GetString(options.ProviderExtendedConfig, \"siteId\"),\n\t\t\tCertificateId:            xmaps.GetString(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t})\n\t\treturn deployer, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_cpanel.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/cpanel\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeCPanel, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForCPanel{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := cpanel.NewDeployer(&cpanel.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tUsername:                 credentials.Username,\n\t\t\tApiToken:                 credentials.ApiToken,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tResourceType:             xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tDomain:                   xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ctcccloud_ao.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tctcccloudao \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-ao\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeCTCCCloudAO, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForCTCCCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ctcccloudao.NewDeployer(&ctcccloudao.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tSecretAccessKey:    credentials.SecretAccessKey,\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ctcccloud_cdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tctcccloudcdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-cdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeCTCCCloudCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForCTCCCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ctcccloudcdn.NewDeployer(&ctcccloudcdn.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tSecretAccessKey:    credentials.SecretAccessKey,\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ctcccloud_cms.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tctcccloudcms \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-cms\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeCTCCCloudCMS, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForCTCCCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ctcccloudcms.NewDeployer(&ctcccloudcms.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tSecretAccessKey: credentials.SecretAccessKey,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ctcccloud_elb.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tctcccloudelb \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-elb\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeCTCCCloudELB, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForCTCCCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ctcccloudelb.NewDeployer(&ctcccloudelb.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tSecretAccessKey: credentials.SecretAccessKey,\n\t\t\tRegionId:        xmaps.GetString(options.ProviderExtendedConfig, \"regionId\"),\n\t\t\tResourceType:    xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tLoadbalancerId:  xmaps.GetString(options.ProviderExtendedConfig, \"loadbalancerId\"),\n\t\t\tListenerId:      xmaps.GetString(options.ProviderExtendedConfig, \"listenerId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ctcccloud_faas.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tctcccloudfaas \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-faas\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeCTCCCloudFaaS, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForCTCCCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ctcccloudfaas.NewDeployer(&ctcccloudfaas.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tSecretAccessKey: credentials.SecretAccessKey,\n\t\t\tRegionId:        xmaps.GetString(options.ProviderExtendedConfig, \"regionId\"),\n\t\t\tDomain:          xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ctcccloud_icdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tctcccloudicdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-icdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeCTCCCloudICDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForCTCCCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ctcccloudicdn.NewDeployer(&ctcccloudicdn.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tSecretAccessKey:    credentials.SecretAccessKey,\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ctcccloud_lvdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tctcccloudlvdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-lvdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeCTCCCloudLVDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForCTCCCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ctcccloudlvdn.NewDeployer(&ctcccloudlvdn.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tSecretAccessKey:    credentials.SecretAccessKey,\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_dogecloud_cdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tpDogeCDN \"github.com/certimate-go/certimate/pkg/core/deployer/providers/dogecloud-cdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeDogeCloudCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForDogeCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := pDogeCDN.NewDeployer(&pDogeCDN.DeployerConfig{\n\t\t\tAccessKey:          credentials.AccessKey,\n\t\t\tSecretKey:          credentials.SecretKey,\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_dokploy.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/dokploy\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeDokploy, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForDokploy{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := dokploy.NewDeployer(&dokploy.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiKey:                   credentials.ApiKey,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_flexcdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/flexcdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeFlexCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForFlexCDN{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := flexcdn.NewDeployer(&flexcdn.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiRole:                  credentials.ApiRole,\n\t\t\tAccessKeyId:              credentials.AccessKeyId,\n\t\t\tAccessKey:                credentials.AccessKey,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tResourceType:             xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tCertificateId:            xmaps.GetInt64(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_flyio.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tflyio \"github.com/certimate-go/certimate/pkg/core/deployer/providers/flyio\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeFlyIO, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForFlyIO{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := flyio.NewDeployer(&flyio.DeployerConfig{\n\t\t\tApiToken: credentials.ApiToken,\n\t\t\tAppName:  xmaps.GetString(options.ProviderExtendedConfig, \"appName\"),\n\t\t\tDomain:   xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_gcore_cdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tgcorecdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/gcore-cdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeGcoreCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForGcore{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := gcorecdn.NewDeployer(&gcorecdn.DeployerConfig{\n\t\t\tApiToken:      credentials.ApiToken,\n\t\t\tResourceId:    xmaps.GetInt64(options.ProviderExtendedConfig, \"resourceId\"),\n\t\t\tCertificateId: xmaps.GetInt64(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_goedge.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/goedge\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeGoEdge, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForGoEdge{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := goedge.NewDeployer(&goedge.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiRole:                  credentials.ApiRole,\n\t\t\tAccessKeyId:              credentials.AccessKeyId,\n\t\t\tAccessKey:                credentials.AccessKey,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tResourceType:             xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tCertificateId:            xmaps.GetInt64(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_huaweicloud_cdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\thuaweicloudcdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-cdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeHuaweiCloudCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForHuaweiCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := huaweicloudcdn.NewDeployer(&huaweicloudcdn.DeployerConfig{\n\t\t\tAccessKeyId:         credentials.AccessKeyId,\n\t\t\tSecretAccessKey:     credentials.SecretAccessKey,\n\t\t\tEnterpriseProjectId: credentials.EnterpriseProjectId,\n\t\t\tRegion:              xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tDomainMatchPattern:  xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:              xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_huaweicloud_elb.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\thuaweicloudelb \"github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-elb\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeHuaweiCloudELB, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForHuaweiCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := huaweicloudelb.NewDeployer(&huaweicloudelb.DeployerConfig{\n\t\t\tAccessKeyId:         credentials.AccessKeyId,\n\t\t\tSecretAccessKey:     credentials.SecretAccessKey,\n\t\t\tEnterpriseProjectId: credentials.EnterpriseProjectId,\n\t\t\tRegion:              xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tResourceType:        xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tCertificateId:       xmaps.GetString(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t\tLoadbalancerId:      xmaps.GetString(options.ProviderExtendedConfig, \"loadbalancerId\"),\n\t\t\tListenerId:          xmaps.GetString(options.ProviderExtendedConfig, \"listenerId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_huaweicloud_obs.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\thuaweicloudobs \"github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-obs\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeHuaweiCloudOBS, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForHuaweiCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := huaweicloudobs.NewDeployer(&huaweicloudobs.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tSecretAccessKey: credentials.SecretAccessKey,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tBucket:          xmaps.GetString(options.ProviderExtendedConfig, \"bucket\"),\n\t\t\tDomain:          xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_huaweicloud_scm.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\thuaweicloudscm \"github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-scm\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeHuaweiCloudSCM, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForHuaweiCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := huaweicloudscm.NewDeployer(&huaweicloudscm.DeployerConfig{\n\t\t\tAccessKeyId:         credentials.AccessKeyId,\n\t\t\tSecretAccessKey:     credentials.SecretAccessKey,\n\t\t\tEnterpriseProjectId: credentials.EnterpriseProjectId,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_huaweicloud_waf.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\thuaweicloudwaf \"github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-waf\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeHuaweiCloudWAF, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForHuaweiCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := huaweicloudwaf.NewDeployer(&huaweicloudwaf.DeployerConfig{\n\t\t\tAccessKeyId:         credentials.AccessKeyId,\n\t\t\tSecretAccessKey:     credentials.SecretAccessKey,\n\t\t\tEnterpriseProjectId: credentials.EnterpriseProjectId,\n\t\t\tRegion:              xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tResourceType:        xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tCertificateId:       xmaps.GetString(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t\tDomain:              xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_jdcloud_alb.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tjdcloudalb \"github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-alb\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeJDCloudALB, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForJDCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := jdcloudalb.NewDeployer(&jdcloudalb.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.AccessKeySecret,\n\t\t\tRegionId:        xmaps.GetString(options.ProviderExtendedConfig, \"regionId\"),\n\t\t\tResourceType:    xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tLoadbalancerId:  xmaps.GetString(options.ProviderExtendedConfig, \"loadbalancerId\"),\n\t\t\tListenerId:      xmaps.GetString(options.ProviderExtendedConfig, \"listenerId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_jdcloud_cdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tjdcloudcdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-cdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeJDCloudCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForJDCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := jdcloudcdn.NewDeployer(&jdcloudcdn.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.AccessKeySecret,\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_jdcloud_live.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tjdcloudlive \"github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-live\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeJDCloudLive, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForJDCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := jdcloudlive.NewDeployer(&jdcloudlive.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.AccessKeySecret,\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_jdcloud_vod.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tjdcloudvod \"github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-vod\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeJDCloudVOD, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForJDCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := jdcloudvod.NewDeployer(&jdcloudvod.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.AccessKeySecret,\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_kong.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/kong\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeKong, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForKong{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := kong.NewDeployer(&kong.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiToken:                 credentials.ApiToken,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tResourceType:             xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tWorkspace:                xmaps.GetString(options.ProviderExtendedConfig, \"workspace\"),\n\t\t\tCertificateId:            xmaps.GetString(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ksyun_cdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tksyuncdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ksyun-cdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeKsyunCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForKsyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ksyuncdn.NewDeployer(&ksyuncdn.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tSecretAccessKey:    credentials.SecretAccessKey,\n\t\t\tResourceType:       xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t\tCertificateId:      xmaps.GetString(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_kubernetes_secret.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tk8ssecret \"github.com/certimate-go/certimate/pkg/core/deployer/providers/k8s-secret\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeKubernetesSecret, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForKubernetes{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tparseKeyValueMap := func(s string) (map[string]string, error) {\n\t\t\tresult := make(map[string]string)\n\n\t\t\tlines := strings.Split(s, \"\\n\")\n\t\t\tfor i, line := range lines {\n\t\t\t\tif strings.TrimSpace(line) == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tpos := strings.Index(line, \":\")\n\t\t\t\tif pos == -1 {\n\t\t\t\t\treturn nil, fmt.Errorf(\"invalid line format at line %d\", i+1)\n\t\t\t\t}\n\n\t\t\t\tkey := strings.TrimSpace(line[:pos])\n\t\t\t\tvalue := strings.TrimSpace(line[pos+1:])\n\t\t\t\tif key == \"\" {\n\t\t\t\t\treturn nil, fmt.Errorf(\"invalid key at line %d\", i+1)\n\t\t\t\t}\n\n\t\t\t\tresult[key] = value\n\t\t\t}\n\n\t\t\treturn result, nil\n\t\t}\n\n\t\tsecretAnnotations := make(map[string]string)\n\t\tif secretAnnotationsString := xmaps.GetString(options.ProviderExtendedConfig, \"secretAnnotations\"); secretAnnotationsString != \"\" {\n\t\t\ttemp, err := parseKeyValueMap(secretAnnotationsString)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to parse kubernetes secret annotations: %w\", err)\n\t\t\t}\n\t\t\tsecretAnnotations = temp\n\t\t}\n\n\t\tsecretLabels := make(map[string]string)\n\t\tif secretLabelsString := xmaps.GetString(options.ProviderExtendedConfig, \"secretLabels\"); secretLabelsString != \"\" {\n\t\t\ttemp, err := parseKeyValueMap(secretLabelsString)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to parse kubernetes secret labels: %w\", err)\n\t\t\t}\n\t\t\tsecretLabels = temp\n\t\t}\n\n\t\tprovider, err := k8ssecret.NewDeployer(&k8ssecret.DeployerConfig{\n\t\t\tKubeConfig:          credentials.KubeConfig,\n\t\t\tNamespace:           xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"namespace\", \"default\"),\n\t\t\tSecretName:          xmaps.GetString(options.ProviderExtendedConfig, \"secretName\"),\n\t\t\tSecretType:          xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"secretType\", \"kubernetes.io/tls\"),\n\t\t\tSecretDataKeyForCrt: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"secretDataKeyForCrt\", \"tls.crt\"),\n\t\t\tSecretDataKeyForKey: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"secretDataKeyForKey\", \"tls.key\"),\n\t\t\tSecretAnnotations:   secretAnnotations,\n\t\t\tSecretLabels:        secretLabels,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_lecdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/lecdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeLeCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForLeCDN{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := lecdn.NewDeployer(&lecdn.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiVersion:               credentials.ApiVersion,\n\t\t\tApiRole:                  credentials.ApiRole,\n\t\t\tUsername:                 credentials.Username,\n\t\t\tPassword:                 credentials.Password,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tResourceType:             xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tCertificateId:            xmaps.GetInt64(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t\tClientId:                 xmaps.GetInt64(options.ProviderExtendedConfig, \"clientId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_local.go",
    "content": "package deployers\n\nimport (\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/local\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeLocal, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tprovider, err := local.NewDeployer(&local.DeployerConfig{\n\t\t\tShellEnv:                 xmaps.GetString(options.ProviderExtendedConfig, \"shellEnv\"),\n\t\t\tPreCommand:               xmaps.GetString(options.ProviderExtendedConfig, \"preCommand\"),\n\t\t\tPostCommand:              xmaps.GetString(options.ProviderExtendedConfig, \"postCommand\"),\n\t\t\tOutputFormat:             xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"format\", local.OUTPUT_FORMAT_PEM),\n\t\t\tOutputCertPath:           xmaps.GetString(options.ProviderExtendedConfig, \"certPath\"),\n\t\t\tOutputServerCertPath:     xmaps.GetString(options.ProviderExtendedConfig, \"certPathForServerOnly\"),\n\t\t\tOutputIntermediaCertPath: xmaps.GetString(options.ProviderExtendedConfig, \"certPathForIntermediaOnly\"),\n\t\t\tOutputKeyPath:            xmaps.GetString(options.ProviderExtendedConfig, \"keyPath\"),\n\t\t\tPfxPassword:              xmaps.GetString(options.ProviderExtendedConfig, \"pfxPassword\"),\n\t\t\tJksAlias:                 xmaps.GetString(options.ProviderExtendedConfig, \"jksAlias\"),\n\t\t\tJksKeypass:               xmaps.GetString(options.ProviderExtendedConfig, \"jksKeypass\"),\n\t\t\tJksStorepass:             xmaps.GetString(options.ProviderExtendedConfig, \"jksStorepass\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_mohua_mvh.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tmohuamvh \"github.com/certimate-go/certimate/pkg/core/deployer/providers/mohua-mvh\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeMohuaMVH, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForMohua{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := mohuamvh.NewDeployer(&mohuamvh.DeployerConfig{\n\t\t\tUsername:    credentials.Username,\n\t\t\tApiPassword: credentials.ApiPassword,\n\t\t\tHostId:      xmaps.GetString(options.ProviderExtendedConfig, \"hostId\"),\n\t\t\tDomainId:    xmaps.GetString(options.ProviderExtendedConfig, \"domainId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_netlify.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tnetlify \"github.com/certimate-go/certimate/pkg/core/deployer/providers/netlify\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeNetlify, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForNetlify{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := netlify.NewDeployer(&netlify.DeployerConfig{\n\t\t\tApiToken:     credentials.ApiToken,\n\t\t\tResourceType: xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tSiteId:       xmaps.GetString(options.ProviderExtendedConfig, \"siteId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_nginxproxymanager.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tnginxproxymanager \"github.com/certimate-go/certimate/pkg/core/deployer/providers/nginxproxymanager\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeNginxProxyManager, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForNginxProxyManager{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := nginxproxymanager.NewDeployer(&nginxproxymanager.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tAuthMethod:               credentials.AuthMethod,\n\t\t\tUsername:                 credentials.Username,\n\t\t\tPassword:                 credentials.Password,\n\t\t\tApiToken:                 credentials.ApiToken,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tResourceType:             xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tHostType:                 xmaps.GetString(options.ProviderExtendedConfig, \"hostType\"),\n\t\t\tHostMatchPattern:         xmaps.GetString(options.ProviderExtendedConfig, \"hostMatchPattern\"),\n\t\t\tHostId:                   xmaps.GetInt64(options.ProviderExtendedConfig, \"hostId\"),\n\t\t\tCertificateId:            xmaps.GetInt64(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_proxmoxve.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/proxmoxve\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeProxmoxVE, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForProxmoxVE{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := proxmoxve.NewDeployer(&proxmoxve.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiToken:                 credentials.ApiToken,\n\t\t\tApiTokenSecret:           credentials.ApiTokenSecret,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tNodeName:                 xmaps.GetString(options.ProviderExtendedConfig, \"nodeName\"),\n\t\t\tAutoRestart:              xmaps.GetBool(options.ProviderExtendedConfig, \"autoRestart\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_qiniu_cdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tqiniucdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-cdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeQiniuCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForQiniu{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := qiniucdn.NewDeployer(&qiniucdn.DeployerConfig{\n\t\t\tAccessKey:          credentials.AccessKey,\n\t\t\tSecretKey:          credentials.SecretKey,\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_qiniu_kodo.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tqiniukodo \"github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-kodo\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeQiniuKodo, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForQiniu{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := qiniukodo.NewDeployer(&qiniukodo.DeployerConfig{\n\t\t\tAccessKey: credentials.AccessKey,\n\t\t\tSecretKey: credentials.SecretKey,\n\t\t\tBucket:    xmaps.GetString(options.ProviderExtendedConfig, \"bucket\"),\n\t\t\tDomain:    xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_qiniu_pili.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tqiniupili \"github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-pili\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeQiniuPili, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForQiniu{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := qiniupili.NewDeployer(&qiniupili.DeployerConfig{\n\t\t\tAccessKey:          credentials.AccessKey,\n\t\t\tSecretKey:          credentials.SecretKey,\n\t\t\tHub:                xmaps.GetString(options.ProviderExtendedConfig, \"hub\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_rainyun_rcdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\trainyunrcdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/rainyun-rcdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeRainYunRCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForRainYun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := rainyunrcdn.NewDeployer(&rainyunrcdn.DeployerConfig{\n\t\t\tApiKey:             credentials.ApiKey,\n\t\t\tInstanceId:         xmaps.GetInt64(options.ProviderExtendedConfig, \"instanceId\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_rainyun_sslcenter.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\trainyunsslcenter \"github.com/certimate-go/certimate/pkg/core/deployer/providers/rainyun-sslcenter\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeRainYunSSLCenter, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForRainYun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := rainyunsslcenter.NewDeployer(&rainyunsslcenter.DeployerConfig{\n\t\t\tApiKey:        credentials.ApiKey,\n\t\t\tCertificateId: xmaps.GetInt64(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ratpanel.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tratpanel \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ratpanel\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeRatPanel, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForRatPanel{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ratpanel.NewDeployer(&ratpanel.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tAccessTokenId:            credentials.AccessTokenId,\n\t\t\tAccessToken:              credentials.AccessToken,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tResourceType:             xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tSiteNames:                lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, \"siteNames\"), \";\"), func(s string, _ int) bool { return s != \"\" }),\n\t\t\tCertificateId:            xmaps.GetInt64(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ratpanel_console.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tratpanelconsole \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ratpanel-console\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeRatPanelConsole, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForRatPanel{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ratpanelconsole.NewDeployer(&ratpanelconsole.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tAccessTokenId:            credentials.AccessTokenId,\n\t\t\tAccessToken:              credentials.AccessToken,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_s3.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/s3\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeS3, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForS3{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := s3.NewDeployer(&s3.DeployerConfig{\n\t\t\tEndpoint:                      credentials.Endpoint,\n\t\t\tAccessKey:                     credentials.AccessKey,\n\t\t\tSecretKey:                     credentials.SecretKey,\n\t\t\tSignatureVersion:              credentials.SignatureVersion,\n\t\t\tUsePathStyle:                  credentials.UsePathStyle,\n\t\t\tAllowInsecureConnections:      credentials.AllowInsecureConnections,\n\t\t\tRegion:                        xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tBucket:                        xmaps.GetString(options.ProviderExtendedConfig, \"bucket\"),\n\t\t\tOutputFormat:                  xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"format\", s3.OUTPUT_FORMAT_PEM),\n\t\t\tOutputCertObjectKey:           xmaps.GetString(options.ProviderExtendedConfig, \"certObjectKey\"),\n\t\t\tOutputServerCertObjectKey:     xmaps.GetString(options.ProviderExtendedConfig, \"certObjectKeyForServerOnly\"),\n\t\t\tOutputIntermediaCertObjectKey: xmaps.GetString(options.ProviderExtendedConfig, \"certObjectKeyForIntermediaOnly\"),\n\t\t\tOutputKeyObjectKey:            xmaps.GetString(options.ProviderExtendedConfig, \"keyObjectKey\"),\n\t\t\tPfxPassword:                   xmaps.GetString(options.ProviderExtendedConfig, \"pfxPassword\"),\n\t\t\tJksAlias:                      xmaps.GetString(options.ProviderExtendedConfig, \"jksAlias\"),\n\t\t\tJksKeypass:                    xmaps.GetString(options.ProviderExtendedConfig, \"jksKeypass\"),\n\t\t\tJksStorepass:                  xmaps.GetString(options.ProviderExtendedConfig, \"jksStorepass\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_safeline.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/safeline\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeSafeLine, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForSafeLine{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := safeline.NewDeployer(&safeline.DeployerConfig{\n\t\t\tServerUrl:                credentials.ServerUrl,\n\t\t\tApiToken:                 credentials.ApiToken,\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t\tResourceType:             xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tCertificateId:            xmaps.GetInt64(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ssh.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/ssh\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeSSH, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForSSH{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tjumpServers := make([]ssh.ServerConfig, len(credentials.JumpServers))\n\t\tfor i, jumpServer := range credentials.JumpServers {\n\t\t\tjumpServers[i] = ssh.ServerConfig{\n\t\t\t\tSshHost:          jumpServer.Host,\n\t\t\t\tSshPort:          jumpServer.Port,\n\t\t\t\tSshAuthMethod:    jumpServer.AuthMethod,\n\t\t\t\tSshUsername:      jumpServer.Username,\n\t\t\t\tSshPassword:      jumpServer.Password,\n\t\t\t\tSshKey:           jumpServer.Key,\n\t\t\t\tSshKeyPassphrase: jumpServer.KeyPassphrase,\n\t\t\t}\n\t\t}\n\n\t\tprovider, err := ssh.NewDeployer(&ssh.DeployerConfig{\n\t\t\tServerConfig: ssh.ServerConfig{\n\t\t\t\tSshHost:          credentials.Host,\n\t\t\t\tSshPort:          credentials.Port,\n\t\t\t\tSshAuthMethod:    credentials.AuthMethod,\n\t\t\t\tSshUsername:      credentials.Username,\n\t\t\t\tSshPassword:      credentials.Password,\n\t\t\t\tSshKey:           credentials.Key,\n\t\t\t\tSshKeyPassphrase: credentials.KeyPassphrase,\n\t\t\t},\n\t\t\tJumpServers:              jumpServers,\n\t\t\tUseSCP:                   xmaps.GetBool(options.ProviderExtendedConfig, \"useSCP\"),\n\t\t\tPreCommand:               xmaps.GetString(options.ProviderExtendedConfig, \"preCommand\"),\n\t\t\tPostCommand:              xmaps.GetString(options.ProviderExtendedConfig, \"postCommand\"),\n\t\t\tOutputFormat:             xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"format\", ssh.OUTPUT_FORMAT_PEM),\n\t\t\tOutputKeyPath:            xmaps.GetString(options.ProviderExtendedConfig, \"keyPath\"),\n\t\t\tOutputCertPath:           xmaps.GetString(options.ProviderExtendedConfig, \"certPath\"),\n\t\t\tOutputServerCertPath:     xmaps.GetString(options.ProviderExtendedConfig, \"certPathForServerOnly\"),\n\t\t\tOutputIntermediaCertPath: xmaps.GetString(options.ProviderExtendedConfig, \"certPathForIntermediaOnly\"),\n\t\t\tPfxPassword:              xmaps.GetString(options.ProviderExtendedConfig, \"pfxPassword\"),\n\t\t\tJksAlias:                 xmaps.GetString(options.ProviderExtendedConfig, \"jksAlias\"),\n\t\t\tJksKeypass:               xmaps.GetString(options.ProviderExtendedConfig, \"jksKeypass\"),\n\t\t\tJksStorepass:             xmaps.GetString(options.ProviderExtendedConfig, \"jksStorepass\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_synologydsm.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/synologydsm\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeSynologyDSM, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForSynologyDSM{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := synologydsm.NewDeployer(&synologydsm.DeployerConfig{\n\t\t\tServerUrl:                  credentials.ServerUrl,\n\t\t\tUsername:                   credentials.Username,\n\t\t\tPassword:                   credentials.Password,\n\t\t\tTotpSecret:                 credentials.TotpSecret,\n\t\t\tAllowInsecureConnections:   credentials.AllowInsecureConnections,\n\t\t\tCertificateIdOrDescription: xmaps.GetString(options.ProviderExtendedConfig, \"certificateIdOrDesc\"),\n\t\t\tIsDefault:                  xmaps.GetBool(options.ProviderExtendedConfig, \"isDefault\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_tencentcloud_cdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\ttencentcloudcdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-cdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeTencentCloudCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTencentCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := tencentcloudcdn.NewDeployer(&tencentcloudcdn.DeployerConfig{\n\t\t\tSecretId:           credentials.SecretId,\n\t\t\tSecretKey:          credentials.SecretKey,\n\t\t\tEndpoint:           xmaps.GetString(options.ProviderExtendedConfig, \"endpoint\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_tencentcloud_clb.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\ttencentcloudclb \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-clb\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeTencentCloudCLB, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTencentCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := tencentcloudclb.NewDeployer(&tencentcloudclb.DeployerConfig{\n\t\t\tSecretId:       credentials.SecretId,\n\t\t\tSecretKey:      credentials.SecretKey,\n\t\t\tEndpoint:       xmaps.GetString(options.ProviderExtendedConfig, \"endpoint\"),\n\t\t\tRegion:         xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tResourceType:   xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tLoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, \"loadbalancerId\"),\n\t\t\tListenerId:     xmaps.GetString(options.ProviderExtendedConfig, \"listenerId\"),\n\t\t\tDomain:         xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_tencentcloud_cos.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\ttencentcloudcos \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-cos\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeTencentCloudCOS, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTencentCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := tencentcloudcos.NewDeployer(&tencentcloudcos.DeployerConfig{\n\t\t\tSecretId:  credentials.SecretId,\n\t\t\tSecretKey: credentials.SecretKey,\n\t\t\tRegion:    xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tBucket:    xmaps.GetString(options.ProviderExtendedConfig, \"bucket\"),\n\t\t\tDomain:    xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_tencentcloud_css.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\ttencentcloudcss \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-css\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeTencentCloudCSS, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTencentCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := tencentcloudcss.NewDeployer(&tencentcloudcss.DeployerConfig{\n\t\t\tSecretId:           credentials.SecretId,\n\t\t\tSecretKey:          credentials.SecretKey,\n\t\t\tEndpoint:           xmaps.GetString(options.ProviderExtendedConfig, \"endpoint\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_tencentcloud_ecdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\ttencentcloudecdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ecdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeTencentCloudECDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTencentCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := tencentcloudecdn.NewDeployer(&tencentcloudecdn.DeployerConfig{\n\t\t\tSecretId:           credentials.SecretId,\n\t\t\tSecretKey:          credentials.SecretKey,\n\t\t\tEndpoint:           xmaps.GetString(options.ProviderExtendedConfig, \"endpoint\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_tencentcloud_eo.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\ttencentcloudeo \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-eo\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeTencentCloudEO, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTencentCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := tencentcloudeo.NewDeployer(&tencentcloudeo.DeployerConfig{\n\t\t\tSecretId:           credentials.SecretId,\n\t\t\tSecretKey:          credentials.SecretKey,\n\t\t\tEndpoint:           xmaps.GetString(options.ProviderExtendedConfig, \"endpoint\"),\n\t\t\tZoneId:             xmaps.GetString(options.ProviderExtendedConfig, \"zoneId\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomains:            lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, \"domains\"), \";\"), func(s string, _ int) bool { return s != \"\" }),\n\t\t\tEnableMultipleSSL:  xmaps.GetBool(options.ProviderExtendedConfig, \"enableMultipleSSL\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_tencentcloud_gaap.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\ttencentcloudgaap \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-gaap\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeTencentCloudGAAP, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTencentCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := tencentcloudgaap.NewDeployer(&tencentcloudgaap.DeployerConfig{\n\t\t\tSecretId:     credentials.SecretId,\n\t\t\tSecretKey:    credentials.SecretKey,\n\t\t\tEndpoint:     xmaps.GetString(options.ProviderExtendedConfig, \"endpoint\"),\n\t\t\tResourceType: xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tProxyId:      xmaps.GetString(options.ProviderExtendedConfig, \"proxyId\"),\n\t\t\tListenerId:   xmaps.GetString(options.ProviderExtendedConfig, \"listenerId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_tencentcloud_scf.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\ttencentcloudscf \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-scf\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeTencentCloudSCF, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTencentCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := tencentcloudscf.NewDeployer(&tencentcloudscf.DeployerConfig{\n\t\t\tSecretId:           credentials.SecretId,\n\t\t\tSecretKey:          credentials.SecretKey,\n\t\t\tEndpoint:           xmaps.GetString(options.ProviderExtendedConfig, \"endpoint\"),\n\t\t\tRegion:             xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_tencentcloud_ssl.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\ttencentcloudssl \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ssl\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeTencentCloudSSL, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTencentCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := tencentcloudssl.NewDeployer(&tencentcloudssl.DeployerConfig{\n\t\t\tSecretId:  credentials.SecretId,\n\t\t\tSecretKey: credentials.SecretKey,\n\t\t\tEndpoint:  xmaps.GetString(options.ProviderExtendedConfig, \"endpoint\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_tencentcloud_ssldeploy.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\ttencentcloudssldeploy \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ssl-deploy\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeTencentCloudSSLDeploy, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTencentCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := tencentcloudssldeploy.NewDeployer(&tencentcloudssldeploy.DeployerConfig{\n\t\t\tSecretId:        credentials.SecretId,\n\t\t\tSecretKey:       credentials.SecretKey,\n\t\t\tEndpoint:        xmaps.GetString(options.ProviderExtendedConfig, \"endpoint\"),\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tResourceProduct: xmaps.GetString(options.ProviderExtendedConfig, \"resourceProduct\"),\n\t\t\tResourceIds:     lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, \"resourceIds\"), \";\"), func(s string, _ int) bool { return s != \"\" }),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_tencentcloud_sslupdate.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\ttencentcloudsslupdate \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ssl-update\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeTencentCloudSSLUpdate, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTencentCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := tencentcloudsslupdate.NewDeployer(&tencentcloudsslupdate.DeployerConfig{\n\t\t\tSecretId:         credentials.SecretId,\n\t\t\tSecretKey:        credentials.SecretKey,\n\t\t\tEndpoint:         xmaps.GetString(options.ProviderExtendedConfig, \"endpoint\"),\n\t\t\tCertificateId:    xmaps.GetString(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t\tIsReplaced:       xmaps.GetBool(options.ProviderExtendedConfig, \"isReplaced\"),\n\t\t\tResourceProducts: lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, \"resourceProducts\"), \";\"), func(s string, _ int) bool { return s != \"\" }),\n\t\t\tResourceRegions:  lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, \"resourceRegions\"), \";\"), func(s string, _ int) bool { return s != \"\" }),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_tencentcloud_vod.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\ttencentcloudvod \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-vod\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeTencentCloudVOD, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTencentCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := tencentcloudvod.NewDeployer(&tencentcloudvod.DeployerConfig{\n\t\t\tSecretId:           credentials.SecretId,\n\t\t\tSecretKey:          credentials.SecretKey,\n\t\t\tEndpoint:           xmaps.GetString(options.ProviderExtendedConfig, \"endpoint\"),\n\t\t\tSubAppId:           xmaps.GetInt64(options.ProviderExtendedConfig, \"subAppId\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_tencentcloud_waf.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\ttencentcloudwaf \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-waf\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeTencentCloudWAF, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTencentCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := tencentcloudwaf.NewDeployer(&tencentcloudwaf.DeployerConfig{\n\t\t\tSecretId:   credentials.SecretId,\n\t\t\tSecretKey:  credentials.SecretKey,\n\t\t\tEndpoint:   xmaps.GetString(options.ProviderExtendedConfig, \"endpoint\"),\n\t\t\tRegion:     xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tDomain:     xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t\tDomainId:   xmaps.GetString(options.ProviderExtendedConfig, \"domainId\"),\n\t\t\tInstanceId: xmaps.GetString(options.ProviderExtendedConfig, \"instanceId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ucloud_ualb.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tucloudualb \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-ualb\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeUCloudUALB, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForUCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ucloudualb.NewDeployer(&ucloudualb.DeployerConfig{\n\t\t\tPrivateKey:     credentials.PrivateKey,\n\t\t\tPublicKey:      credentials.PublicKey,\n\t\t\tProjectId:      credentials.ProjectId,\n\t\t\tRegion:         xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tResourceType:   xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tLoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, \"loadbalancerId\"),\n\t\t\tListenerId:     xmaps.GetString(options.ProviderExtendedConfig, \"listenerId\"),\n\t\t\tDomain:         xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ucloud_ucdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tuclouducdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-ucdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeUCloudUCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForUCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := uclouducdn.NewDeployer(&uclouducdn.DeployerConfig{\n\t\t\tPrivateKey: credentials.PrivateKey,\n\t\t\tPublicKey:  credentials.PublicKey,\n\t\t\tProjectId:  credentials.ProjectId,\n\t\t\tDomainId:   xmaps.GetString(options.ProviderExtendedConfig, \"domainId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ucloud_uclb.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tuclouduclb \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-uclb\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeUCloudUCLB, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForUCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := uclouduclb.NewDeployer(&uclouduclb.DeployerConfig{\n\t\t\tPrivateKey:     credentials.PrivateKey,\n\t\t\tPublicKey:      credentials.PublicKey,\n\t\t\tProjectId:      credentials.ProjectId,\n\t\t\tRegion:         xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tResourceType:   xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tLoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, \"loadbalancerId\"),\n\t\t\tVServerId:      xmaps.GetString(options.ProviderExtendedConfig, \"vserverId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ucloud_uewaf.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tuclouduewaf \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-uewaf\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeUCloudUEWAF, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForUCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := uclouduewaf.NewDeployer(&uclouduewaf.DeployerConfig{\n\t\t\tPrivateKey: credentials.PrivateKey,\n\t\t\tPublicKey:  credentials.PublicKey,\n\t\t\tProjectId:  credentials.ProjectId,\n\t\t\tDomain:     xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ucloud_upathx.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tucloudupathx \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-upathx\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeUCloudUPathX, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForUCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ucloudupathx.NewDeployer(&ucloudupathx.DeployerConfig{\n\t\t\tPrivateKey:    credentials.PrivateKey,\n\t\t\tPublicKey:     credentials.PublicKey,\n\t\t\tProjectId:     credentials.ProjectId,\n\t\t\tAcceleratorId: xmaps.GetString(options.ProviderExtendedConfig, \"acceleratorId\"),\n\t\t\tListenerPort:  xmaps.GetInt32(options.ProviderExtendedConfig, \"listenerPort\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_ucloud_us3.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tucloudus3 \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-us3\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeUCloudUS3, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForUCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := ucloudus3.NewDeployer(&ucloudus3.DeployerConfig{\n\t\t\tPrivateKey: credentials.PrivateKey,\n\t\t\tPublicKey:  credentials.PublicKey,\n\t\t\tProjectId:  credentials.ProjectId,\n\t\t\tRegion:     xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tBucket:     xmaps.GetString(options.ProviderExtendedConfig, \"bucket\"),\n\t\t\tDomain:     xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_unicloud_webhost.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tunicloudwebhost \"github.com/certimate-go/certimate/pkg/core/deployer/providers/unicloud-webhost\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeUniCloudWebHost, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForUniCloud{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := unicloudwebhost.NewDeployer(&unicloudwebhost.DeployerConfig{\n\t\t\tUsername:      credentials.Username,\n\t\t\tPassword:      credentials.Password,\n\t\t\tSpaceProvider: xmaps.GetString(options.ProviderExtendedConfig, \"spaceProvider\"),\n\t\t\tSpaceId:       xmaps.GetString(options.ProviderExtendedConfig, \"spaceId\"),\n\t\t\tDomain:        xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_upyun_cdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tupyuncdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/upyun-cdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeUpyunCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForUpyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := upyuncdn.NewDeployer(&upyuncdn.DeployerConfig{\n\t\t\tUsername:           credentials.Username,\n\t\t\tPassword:           credentials.Password,\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_upyun_file.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tupyunfile \"github.com/certimate-go/certimate/pkg/core/deployer/providers/upyun-file\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeUpyunFile, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForUpyun{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := upyunfile.NewDeployer(&upyunfile.DeployerConfig{\n\t\t\tUsername: credentials.Username,\n\t\t\tPassword: credentials.Password,\n\t\t\tBucket:   xmaps.GetString(options.ProviderExtendedConfig, \"bucket\"),\n\t\t\tDomain:   xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_volcengine_alb.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tvolcenginealb \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-alb\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeVolcEngineALB, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForVolcEngine{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := volcenginealb.NewDeployer(&volcenginealb.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.SecretAccessKey,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tResourceType:    xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tLoadbalancerId:  xmaps.GetString(options.ProviderExtendedConfig, \"loadbalancerId\"),\n\t\t\tListenerId:      xmaps.GetString(options.ProviderExtendedConfig, \"listenerId\"),\n\t\t\tDomain:          xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_volcengine_cdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tvolcenginecdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-cdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeVolcEngineCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForVolcEngine{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := volcenginecdn.NewDeployer(&volcenginecdn.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.SecretAccessKey,\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_volcengine_certcenter.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tvolcenginecertcenter \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-certcenter\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeVolcEngineCertCenter, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForVolcEngine{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := volcenginecertcenter.NewDeployer(&volcenginecertcenter.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.SecretAccessKey,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_volcengine_clb.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tvolcengineclb \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-clb\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeVolcEngineCLB, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForVolcEngine{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := volcengineclb.NewDeployer(&volcengineclb.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.SecretAccessKey,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tResourceType:    xmaps.GetString(options.ProviderExtendedConfig, \"resourceType\"),\n\t\t\tLoadbalancerId:  xmaps.GetString(options.ProviderExtendedConfig, \"loadbalancerId\"),\n\t\t\tListenerId:      xmaps.GetString(options.ProviderExtendedConfig, \"listenerId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_volcengine_dcdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tvolcenginedcdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-dcdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeVolcEngineDCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForVolcEngine{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := volcenginedcdn.NewDeployer(&volcenginedcdn.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.SecretAccessKey,\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_volcengine_imagex.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tvolcengineimagex \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-imagex\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeVolcEngineImageX, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForVolcEngine{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := volcengineimagex.NewDeployer(&volcengineimagex.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.SecretAccessKey,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tServiceId:       xmaps.GetString(options.ProviderExtendedConfig, \"serviceId\"),\n\t\t\tDomain:          xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_volcengine_live.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tvolcenginelive \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-live\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeVolcEngineLive, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForVolcEngine{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := volcenginelive.NewDeployer(&volcenginelive.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.SecretAccessKey,\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_volcengine_tos.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tvolcenginetos \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-tos\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeVolcEngineTOS, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForVolcEngine{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := volcenginetos.NewDeployer(&volcenginetos.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.SecretAccessKey,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tBucket:          xmaps.GetString(options.ProviderExtendedConfig, \"bucket\"),\n\t\t\tDomain:          xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_volcengine_vod.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tvolcenginevod \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-vod\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeVolcEngineVOD, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForVolcEngine{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := volcenginevod.NewDeployer(&volcenginevod.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.SecretAccessKey,\n\t\t\tSpaceName:          xmaps.GetString(options.ProviderExtendedConfig, \"spaceName\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomainType:         xmaps.GetString(options.ProviderExtendedConfig, \"domainType\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_volcengine_waf.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tvolcenginewaf \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-waf\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeVolcEngineWAF, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForVolcEngine{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := volcenginewaf.NewDeployer(&volcenginewaf.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.SecretAccessKey,\n\t\t\tRegion:          xmaps.GetString(options.ProviderExtendedConfig, \"region\"),\n\t\t\tAccessMode:      xmaps.GetString(options.ProviderExtendedConfig, \"accessMode\"),\n\t\t\tDomain:          xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_wangsu_cdn.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\twangsucdn \"github.com/certimate-go/certimate/pkg/core/deployer/providers/wangsu-cdn\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeWangsuCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForWangsu{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := wangsucdn.NewDeployer(&wangsucdn.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.AccessKeySecret,\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomains:            lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, \"domains\"), \";\"), func(s string, _ int) bool { return s != \"\" }),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_wangsu_cdnpro.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\twangsucdnpro \"github.com/certimate-go/certimate/pkg/core/deployer/providers/wangsu-cdnpro\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeWangsuCDNPro, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForWangsu{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := wangsucdnpro.NewDeployer(&wangsucdnpro.DeployerConfig{\n\t\t\tAccessKeyId:        credentials.AccessKeyId,\n\t\t\tAccessKeySecret:    credentials.AccessKeySecret,\n\t\t\tApiKey:             credentials.ApiKey,\n\t\t\tEnvironment:        xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"environment\", \"production\"),\n\t\t\tDomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, \"domainMatchPattern\"),\n\t\t\tDomain:             xmaps.GetString(options.ProviderExtendedConfig, \"domain\"),\n\t\t\tCertificateId:      xmaps.GetString(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t\tWebhookId:          xmaps.GetString(options.ProviderExtendedConfig, \"webhookId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_wangsu_certificate.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\twangsucertificate \"github.com/certimate-go/certimate/pkg/core/deployer/providers/wangsu-certificate\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeWangsuCertificate, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForWangsu{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := wangsucertificate.NewDeployer(&wangsucertificate.DeployerConfig{\n\t\t\tAccessKeyId:     credentials.AccessKeyId,\n\t\t\tAccessKeySecret: credentials.AccessKeySecret,\n\t\t\tCertificateId:   xmaps.GetString(options.ProviderExtendedConfig, \"certificateId\"),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/certmgmt/deployers/sp_webhook.go",
    "content": "package deployers\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\twebhook \"github.com/certimate-go/certimate/pkg/core/deployer/providers/webhook\"\n\txhttp \"github.com/certimate-go/certimate/pkg/utils/http\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.DeploymentProviderTypeWebhook, func(options *ProviderFactoryOptions) (deployer.Provider, error) {\n\t\tcredentials := domain.AccessConfigForWebhook{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tmergedHeaders := make(map[string]string)\n\t\tif defaultHeadersString := credentials.HeadersString; defaultHeadersString != \"\" {\n\t\t\th, err := xhttp.ParseHeaders(defaultHeadersString)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to parse webhook headers: %w\", err)\n\t\t\t}\n\t\t\tfor key := range h {\n\t\t\t\tmergedHeaders[http.CanonicalHeaderKey(key)] = h.Get(key)\n\t\t\t}\n\t\t}\n\t\tif extendedHeadersString := xmaps.GetString(options.ProviderExtendedConfig, \"headers\"); extendedHeadersString != \"\" {\n\t\t\th, err := xhttp.ParseHeaders(extendedHeadersString)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to parse webhook headers: %w\", err)\n\t\t\t}\n\t\t\tfor key := range h {\n\t\t\t\tmergedHeaders[http.CanonicalHeaderKey(key)] = h.Get(key)\n\t\t\t}\n\t\t}\n\n\t\tprovider, err := webhook.NewDeployer(&webhook.DeployerConfig{\n\t\t\tWebhookUrl:               credentials.Url,\n\t\t\tWebhookData:              xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"webhookData\", credentials.DataString),\n\t\t\tMethod:                   credentials.Method,\n\t\t\tHeaders:                  mergedHeaders,\n\t\t\tTimeout:                  xmaps.GetInt(options.ProviderExtendedConfig, \"timeout\"),\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/domain/access.go",
    "content": "package domain\n\nimport \"time\"\n\nconst CollectionNameAccess = \"access\"\n\ntype Access struct {\n\tMeta\n\tName      string         `db:\"name\"     json:\"name\"`\n\tProvider  string         `db:\"provider\" json:\"provider\"`\n\tConfig    map[string]any `db:\"config\"   json:\"config\"`\n\tReserve   string         `db:\"reserve\"  json:\"reserve,omitempty\"`\n\tDeletedAt *time.Time     `db:\"deleted\" json:\"deleted\"`\n}\n\ntype AccessConfigFor1Panel struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiVersion               string `json:\"apiVersion\"`\n\tApiKey                   string `json:\"apiKey\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigFor35cn struct {\n\tUsername    string `json:\"username\"`\n\tApiPassword string `json:\"apiPassword\"`\n}\n\ntype AccessConfigFor51DNScom struct {\n\tApiKey    string `json:\"apiKey\"`\n\tApiSecret string `json:\"apiSecret\"`\n}\n\ntype AccessConfigForACMEExternalAccountBinding struct {\n\tEabKid     string `json:\"eabKid,omitempty\"`\n\tEabHmacKey string `json:\"eabHmacKey,omitempty\"`\n}\n\ntype AccessConfigForACMECA struct {\n\tAccessConfigForACMEExternalAccountBinding\n\tEndpoint string `json:\"endpoint\"`\n}\n\ntype AccessConfigForACMEDNS struct {\n\tServerUrl   string `json:\"serverUrl\"`\n\tCredentials string `json:\"credentials\"`\n}\n\ntype AccessConfigForACMEHttpReq struct {\n\tEndpoint string `json:\"endpoint\"`\n\tMode     string `json:\"mode,omitempty\"`\n\tUsername string `json:\"username,omitempty\"`\n\tPassword string `json:\"password,omitempty\"`\n}\n\ntype AccessConfigForActalisSSL struct {\n\tAccessConfigForACMEExternalAccountBinding\n}\n\ntype AccessConfigForAkamai struct {\n\tHost         string `json:\"host\"`\n\tClientToken  string `json:\"clientToken\"`\n\tClientSecret string `json:\"clientSecret\"`\n\tAccessToken  string `json:\"accessToken\"`\n}\n\ntype AccessConfigForAliyun struct {\n\tAccessKeyId     string `json:\"accessKeyId\"`\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n}\n\ntype AccessConfigForAPISIX struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiKey                   string `json:\"apiKey\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForArvanCloud struct {\n\tApiKey string `json:\"apiKey\"`\n}\n\ntype AccessConfigForAWS struct {\n\tAccessKeyId     string `json:\"accessKeyId\"`\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n}\n\ntype AccessConfigForAzure struct {\n\tTenantId          string `json:\"tenantId\"`\n\tClientId          string `json:\"clientId\"`\n\tClientSecret      string `json:\"clientSecret\"`\n\tSubscriptionId    string `json:\"subscriptionId,omitempty\"`\n\tResourceGroupName string `json:\"resourceGroupName,omitempty\"`\n\tCloudName         string `json:\"cloudName,omitempty\"`\n}\n\ntype AccessConfigForBaiduCloud struct {\n\tAccessKeyId     string `json:\"accessKeyId\"`\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n}\n\ntype AccessConfigForBaishan struct {\n\tApiToken string `json:\"apiToken\"`\n}\n\ntype AccessConfigForBaotaPanel struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiKey                   string `json:\"apiKey\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForBaotaPanelGo struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiKey                   string `json:\"apiKey\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForBaotaWAF struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiKey                   string `json:\"apiKey\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForBookMyName struct {\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n}\n\ntype AccessConfigForBunny struct {\n\tApiKey string `json:\"apiKey\"`\n}\n\ntype AccessConfigForBytePlus struct {\n\tAccessKey string `json:\"accessKey\"`\n\tSecretKey string `json:\"secretKey\"`\n}\n\ntype AccessConfigForCacheFly struct {\n\tApiToken string `json:\"apiToken\"`\n}\n\ntype AccessConfigForCdnfly struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiKey                   string `json:\"apiKey\"`\n\tApiSecret                string `json:\"apiSecret\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForCloudflare struct {\n\tDnsApiToken  string `json:\"dnsApiToken\"`\n\tZoneApiToken string `json:\"zoneApiToken,omitempty\"`\n}\n\ntype AccessConfigForClouDNS struct {\n\tAuthId       string `json:\"authId\"`\n\tAuthPassword string `json:\"authPassword\"`\n}\n\ntype AccessConfigForCMCCCloud struct {\n\tAccessKeyId     string `json:\"accessKeyId\"`\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n}\n\ntype AccessConfigForConstellix struct {\n\tApiKey    string `json:\"apiKey\"`\n\tSecretKey string `json:\"secretKey\"`\n}\n\ntype AccessConfigForCPanel struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tUsername                 string `json:\"username\"`\n\tApiToken                 string `json:\"apiToken\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForCTCCCloud struct {\n\tAccessKeyId     string `json:\"accessKeyId\"`\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n}\n\ntype AccessConfigForDeSEC struct {\n\tToken string `json:\"token\"`\n}\n\ntype AccessConfigForDigitalOcean struct {\n\tAccessToken string `json:\"accessToken\"`\n}\n\ntype AccessConfigForDingTalkBot struct {\n\tWebhookUrl    string `json:\"webhookUrl\"`\n\tSecret        string `json:\"secret,omitempty\"`\n\tCustomPayload string `json:\"customPayload,omitempty\"`\n}\n\ntype AccessConfigForDiscordBot struct {\n\tBotToken  string `json:\"botToken\"`\n\tChannelId string `json:\"channelId,omitempty\"`\n}\n\ntype AccessConfigForDNSExit struct {\n\tApiKey string `json:\"apiKey\"`\n}\n\ntype AccessConfigForDNSLA struct {\n\tApiId     string `json:\"apiId\"`\n\tApiSecret string `json:\"apiSecret\"`\n}\n\ntype AccessConfigForDNSMadeEasy struct {\n\tApiKey    string `json:\"apiKey\"`\n\tApiSecret string `json:\"apiSecret\"`\n}\n\ntype AccessConfigForDogeCloud struct {\n\tAccessKey string `json:\"accessKey\"`\n\tSecretKey string `json:\"secretKey\"`\n}\n\ntype AccessConfigForDokploy struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiKey                   string `json:\"apiKey\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForDuckDNS struct {\n\tToken string `json:\"token\"`\n}\n\ntype AccessConfigForDynu struct {\n\tApiKey string `json:\"apiKey\"`\n}\n\ntype AccessConfigForDynv6 struct {\n\tHttpToken string `json:\"httpToken\"`\n}\n\ntype AccessConfigForEmail struct {\n\tSmtpHost                 string `json:\"smtpHost\"`\n\tSmtpPort                 int32  `json:\"smtpPort\"`\n\tSmtpTls                  bool   `json:\"smtpTls\"`\n\tUsername                 string `json:\"username\"`\n\tPassword                 string `json:\"password\"`\n\tSenderAddress            string `json:\"senderAddress\"`\n\tSenderName               string `json:\"senderName\"`\n\tReceiverAddress          string `json:\"receiverAddress,omitempty\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForFlexCDN struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiRole                  string `json:\"apiRole\"`\n\tAccessKeyId              string `json:\"accessKeyId\"`\n\tAccessKey                string `json:\"accessKey\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForFlyIO struct {\n\tApiToken string `json:\"apiToken\"`\n}\n\ntype AccessConfigForGandinet struct {\n\tPersonalAccessToken string `json:\"personalAccessToken\"`\n}\n\ntype AccessConfigForGcore struct {\n\tApiToken string `json:\"apiToken\"`\n}\n\ntype AccessConfigForGlobalSectigo struct {\n\tAccessConfigForACMEExternalAccountBinding\n\tValidationType string `json:\"validationType\"`\n}\n\ntype AccessConfigForGlobalSignAtlas struct {\n\tAccessConfigForACMEExternalAccountBinding\n}\n\ntype AccessConfigForGname struct {\n\tAppId  string `json:\"appId\"`\n\tAppKey string `json:\"appKey\"`\n}\n\ntype AccessConfigForGoDaddy struct {\n\tApiKey    string `json:\"apiKey\"`\n\tApiSecret string `json:\"apiSecret\"`\n}\n\ntype AccessConfigForGoEdge struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiRole                  string `json:\"apiRole\"`\n\tAccessKeyId              string `json:\"accessKeyId\"`\n\tAccessKey                string `json:\"accessKey\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForGoogleTrustServices struct {\n\tAccessConfigForACMEExternalAccountBinding\n}\n\ntype AccessConfigForHetzner struct {\n\tApiToken string `json:\"apiToken\"`\n}\n\ntype AccessConfigForHostingde struct {\n\tApiKey string `json:\"apiKey\"`\n}\n\ntype AccessConfigForHostinger struct {\n\tApiToken string `json:\"apiToken\"`\n}\n\ntype AccessConfigForHuaweiCloud struct {\n\tAccessKeyId         string `json:\"accessKeyId\"`\n\tSecretAccessKey     string `json:\"secretAccessKey\"`\n\tEnterpriseProjectId string `json:\"enterpriseProjectId,omitempty\"`\n}\n\ntype AccessConfigForInfomaniak struct {\n\tAccessToken string `json:\"accessToken\"`\n}\n\ntype AccessConfigForIONOS struct {\n\tApiKeyPublicPrefix string `json:\"apiKeyPublicPrefix\"`\n\tApiKeySecret       string `json:\"apiKeySecret\"`\n}\n\ntype AccessConfigForJDCloud struct {\n\tAccessKeyId     string `json:\"accessKeyId\"`\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n}\n\ntype AccessConfigForKong struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiToken                 string `json:\"apiToken,omitempty\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForKubernetes struct {\n\tKubeConfig string `json:\"kubeConfig,omitempty\"`\n}\n\ntype AccessConfigForKsyun struct {\n\tAccessKeyId     string `json:\"accessKeyId\"`\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n}\n\ntype AccessConfigForLarkBot struct {\n\tWebhookUrl    string `json:\"webhookUrl\"`\n\tSecret        string `json:\"secret,omitempty\"`\n\tCustomPayload string `json:\"customPayload,omitempty\"`\n}\n\ntype AccessConfigForLeCDN struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiVersion               string `json:\"apiVersion\"`\n\tApiRole                  string `json:\"apiRole\"`\n\tUsername                 string `json:\"username\"`\n\tPassword                 string `json:\"password\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForLinode struct {\n\tAccessToken string `json:\"accessToken\"`\n}\n\ntype AccessConfigForLiteSSL struct {\n\tAccessConfigForACMEExternalAccountBinding\n}\n\ntype AccessConfigForMattermost struct {\n\tServerUrl string `json:\"serverUrl\"`\n\tUsername  string `json:\"username\"`\n\tPassword  string `json:\"password\"`\n\tChannelId string `json:\"channelId,omitempty\"`\n}\n\ntype AccessConfigForMohua struct {\n\tUsername    string `json:\"username\"`\n\tApiPassword string `json:\"apiPassword\"`\n}\n\ntype AccessConfigForNamecheap struct {\n\tUsername string `json:\"username\"`\n\tApiKey   string `json:\"apiKey\"`\n}\n\ntype AccessConfigForNameDotCom struct {\n\tUsername string `json:\"username\"`\n\tApiToken string `json:\"apiToken\"`\n}\n\ntype AccessConfigForNameSilo struct {\n\tApiKey string `json:\"apiKey\"`\n}\n\ntype AccessConfigForNetcup struct {\n\tCustomerNumber string `json:\"customerNumber\"`\n\tApiKey         string `json:\"apiKey\"`\n\tApiPassword    string `json:\"apiPassword\"`\n}\n\ntype AccessConfigForNetlify struct {\n\tApiToken string `json:\"apiToken\"`\n}\n\ntype AccessConfigForNginxProxyManager struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tAuthMethod               string `json:\"authMethod\"`\n\tUsername                 string `json:\"username,omitempty\"`\n\tPassword                 string `json:\"password,omitempty\"`\n\tApiToken                 string `json:\"apiToken,omitempty\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForNS1 struct {\n\tApiKey string `json:\"apiKey\"`\n}\n\ntype AccessConfigForOVHcloud struct {\n\tEndpoint          string `json:\"endpoint\"`\n\tAuthMethod        string `json:\"authMethod\"`\n\tApplicationKey    string `json:\"applicationKey,omitempty\"`\n\tApplicationSecret string `json:\"applicationSecret,omitempty\"`\n\tConsumerKey       string `json:\"consumerKey,omitempty\"`\n\tClientId          string `json:\"clientId,omitempty\"`\n\tClientSecret      string `json:\"clientSecret,omitempty\"`\n}\n\ntype AccessConfigForPorkbun struct {\n\tApiKey       string `json:\"apiKey\"`\n\tSecretApiKey string `json:\"secretApiKey\"`\n}\n\ntype AccessConfigForPowerDNS struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiKey                   string `json:\"apiKey\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForProxmoxVE struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiToken                 string `json:\"apiToken\"`\n\tApiTokenSecret           string `json:\"apiTokenSecret,omitempty\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForQingCloud struct {\n\tAccessKeyId     string `json:\"accessKeyId\"`\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n}\n\ntype AccessConfigForQiniu struct {\n\tAccessKey string `json:\"accessKey\"`\n\tSecretKey string `json:\"secretKey\"`\n}\n\ntype AccessConfigForRainYun struct {\n\tApiKey string `json:\"apiKey\"`\n}\n\ntype AccessConfigForRatPanel struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tAccessTokenId            int64  `json:\"accessTokenId\"`\n\tAccessToken              string `json:\"accessToken\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForRFC2136 struct {\n\tHost          string `json:\"host\"`\n\tPort          int32  `json:\"port\"`\n\tTsigAlgorithm string `json:\"tsigAlgorithm,omitempty\"`\n\tTsigKey       string `json:\"tsigKey,omitempty\"`\n\tTsigSecret    string `json:\"tsigSecret,omitempty\"`\n}\n\ntype AccessConfigForS3 struct {\n\tEndpoint                 string `json:\"endpoint\"`\n\tAccessKey                string `json:\"accessKey\"`\n\tSecretKey                string `json:\"secretKey\"`\n\tSignatureVersion         string `json:\"signatureVersion,omitempty\"`\n\tUsePathStyle             bool   `json:\"usePathStyle,omitempty\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForSafeLine struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiToken                 string `json:\"apiToken\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForSlackBot struct {\n\tBotToken  string `json:\"botToken\"`\n\tChannelId string `json:\"channelId,omitempty\"`\n}\n\ntype AccessConfigForSpaceship struct {\n\tApiKey    string `json:\"apiKey\"`\n\tApiSecret string `json:\"apiSecret\"`\n}\n\ntype AccessConfigForSSH struct {\n\tHost          string `json:\"host\"`\n\tPort          int32  `json:\"port\"`\n\tAuthMethod    string `json:\"authMethod\"`\n\tUsername      string `json:\"username\"`\n\tPassword      string `json:\"password,omitempty\"`\n\tKey           string `json:\"key,omitempty\"`\n\tKeyPassphrase string `json:\"keyPassphrase,omitempty\"`\n\tJumpServers   []struct {\n\t\tHost          string `json:\"host\"`\n\t\tPort          int32  `json:\"port\"`\n\t\tAuthMethod    string `json:\"authMethod\"`\n\t\tUsername      string `json:\"username\"`\n\t\tPassword      string `json:\"password,omitempty\"`\n\t\tKey           string `json:\"key,omitempty\"`\n\t\tKeyPassphrase string `json:\"keyPassphrase,omitempty\"`\n\t} `json:\"jumpServers,omitempty\"`\n}\n\ntype AccessConfigForSSLCom struct {\n\tAccessConfigForACMEExternalAccountBinding\n}\n\ntype AccessConfigForSynologyDSM struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tUsername                 string `json:\"username\"`\n\tPassword                 string `json:\"password\"`\n\tTotpSecret               string `json:\"totpSecret,omitempty\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForTechnitiumDNS struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiToken                 string `json:\"apiToken\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForTelegramBot struct {\n\tBotToken string `json:\"botToken\"`\n\tChatId   string `json:\"chatId,omitempty\"`\n}\n\ntype AccessConfigForTodayNIC struct {\n\tUserId string `json:\"userId\"`\n\tApiKey string `json:\"apiKey\"`\n}\n\ntype AccessConfigForTencentCloud struct {\n\tSecretId  string `json:\"secretId\"`\n\tSecretKey string `json:\"secretKey\"`\n}\n\ntype AccessConfigForUCloud struct {\n\tPrivateKey string `json:\"privateKey\"`\n\tPublicKey  string `json:\"publicKey\"`\n\tProjectId  string `json:\"projectId,omitempty\"`\n}\n\ntype AccessConfigForUniCloud struct {\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n}\n\ntype AccessConfigForUpyun struct {\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n}\n\ntype AccessConfigForVercel struct {\n\tApiAccessToken string `json:\"apiAccessToken\"`\n\tTeamId         string `json:\"teamId,omitempty\"`\n}\n\ntype AccessConfigForVolcEngine struct {\n\tAccessKeyId     string `json:\"accessKeyId\"`\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n}\n\ntype AccessConfigForVultr struct {\n\tApiKey string `json:\"apiKey\"`\n}\n\ntype AccessConfigForWangsu struct {\n\tAccessKeyId     string `json:\"accessKeyId\"`\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\tApiKey          string `json:\"apiKey\"`\n}\n\ntype AccessConfigForWebhook struct {\n\tUrl                      string `json:\"url\"`\n\tMethod                   string `json:\"method,omitempty\"`\n\tHeadersString            string `json:\"headers,omitempty\"`\n\tDataString               string `json:\"data,omitempty\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype AccessConfigForWeComBot struct {\n\tWebhookUrl    string `json:\"webhookUrl\"`\n\tCustomPayload string `json:\"customPayload,omitempty\"`\n}\n\ntype AccessConfigForWestcn struct {\n\tUsername    string `json:\"username\"`\n\tApiPassword string `json:\"apiPassword\"`\n}\n\ntype AccessConfigForXinnet struct {\n\tAgentId     string `json:\"agentId\"`\n\tApiPassword string `json:\"apiPassword\"`\n}\n\ntype AccessConfigForZeroSSL struct {\n\tAccessConfigForACMEExternalAccountBinding\n}\n"
  },
  {
    "path": "internal/domain/acme_account.go",
    "content": "package domain\n\nimport (\n\t\"crypto\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/registration\"\n\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\nconst CollectionNameACMEAccount = \"acme_accounts\"\n\ntype ACMEAccount struct {\n\tMeta\n\tCA          string        `db:\"ca\"          json:\"ca\"`\n\tEmail       string        `db:\"email\"       json:\"email\"`\n\tPrivateKey  string        `db:\"privateKey\"  json:\"privateKey\"`\n\tACMEAccount *acme.Account `db:\"acmeAccount\" json:\"acmeAccount\"`\n\tACMEAcctUrl string        `db:\"acmeAcctUrl\" json:\"acmeAcctUrl\"`\n\tACMEDirUrl  string        `db:\"acmeDirUrl\"  json:\"acmeDirUrl\"`\n}\n\nfunc (a *ACMEAccount) GetEmail() string {\n\treturn a.Email\n}\n\nfunc (a *ACMEAccount) GetRegistration() *registration.Resource {\n\tif a.ACMEAccount == nil {\n\t\treturn nil\n\t}\n\n\treturn &registration.Resource{\n\t\tBody: *a.ACMEAccount,\n\t\tURI:  a.ACMEAcctUrl,\n\t}\n}\n\nfunc (a *ACMEAccount) GetPrivateKey() crypto.PrivateKey {\n\tif a.PrivateKey == \"\" {\n\t\treturn nil\n\t}\n\n\trs, _ := xcert.ParsePrivateKeyFromPEM(a.PrivateKey)\n\treturn rs\n}\n"
  },
  {
    "path": "internal/domain/certificate.go",
    "content": "package domain\n\nimport (\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcertkey \"github.com/certimate-go/certimate/pkg/utils/cert/key\"\n\txcertx509 \"github.com/certimate-go/certimate/pkg/utils/cert/x509\"\n)\n\nconst CollectionNameCertificate = \"certificate\"\n\ntype Certificate struct {\n\tMeta\n\tSource            CertificateSourceType       `db:\"source\"            json:\"source\"`\n\tSubjectAltNames   string                      `db:\"subjectAltNames\"   json:\"subjectAltNames\"`\n\tSerialNumber      string                      `db:\"serialNumber\"      json:\"serialNumber\"`\n\tCertificate       string                      `db:\"certificate\"       json:\"certificate\"`\n\tPrivateKey        string                      `db:\"privateKey\"        json:\"privateKey\"`\n\tIssuerOrg         string                      `db:\"issuerOrg\"         json:\"issuerOrg\"`\n\tIssuerCertificate string                      `db:\"issuerCertificate\" json:\"issuerCertificate\"`\n\tKeyAlgorithm      CertificateKeyAlgorithmType `db:\"keyAlgorithm\"      json:\"keyAlgorithm\"`\n\tValidityNotBefore time.Time                   `db:\"validityNotBefore\" json:\"validityNotBefore\"`\n\tValidityNotAfter  time.Time                   `db:\"validityNotAfter\"  json:\"validityNotAfter\"`\n\tValidityInterval  int32                       `db:\"validityInterval\"  json:\"validityInterval\"`\n\tACMEAcctUrl       string                      `db:\"acmeAcctUrl\"       json:\"acmeAcctUrl\"`\n\tACMECertUrl       string                      `db:\"acmeCertUrl\"       json:\"acmeCertUrl\"`\n\tIsRenewed         bool                        `db:\"isRenewed\"         json:\"isRenewed\"`\n\tIsRevoked         bool                        `db:\"isRevoked\"         json:\"isRevoked\"`\n\tWorkflowId        string                      `db:\"workflowRef\"       json:\"workflowId\"`\n\tWorkflowRunId     string                      `db:\"workflowRunRef\"    json:\"workflowRunId\"`\n\tWorkflowNodeId    string                      `db:\"workflowNodeId\"    json:\"workflowNodeId\"`\n\tDeletedAt         *time.Time                  `db:\"deleted\" json:\"deleted\"`\n}\n\nfunc (c *Certificate) PopulateFromX509(certX509 *x509.Certificate) *Certificate {\n\tc.SubjectAltNames = strings.Join(xcertx509.GetSubjectAltNames(certX509), \";\")\n\tc.SerialNumber = strings.ToUpper(certX509.SerialNumber.Text(16))\n\tc.IssuerOrg = strings.Join(certX509.Issuer.Organization, \";\")\n\tc.ValidityNotBefore = certX509.NotBefore\n\tc.ValidityNotAfter = certX509.NotAfter\n\tc.ValidityInterval = int32(certX509.NotAfter.Sub(certX509.NotBefore).Seconds())\n\n\tkeyAlgorithm, keySize, _ := xcertkey.GetPublicKeyAlgorithm(certX509.PublicKey)\n\tswitch keyAlgorithm {\n\tcase x509.RSA:\n\t\tc.KeyAlgorithm = CertificateKeyAlgorithmType(fmt.Sprintf(\"RSA%d\", keySize))\n\tcase x509.ECDSA:\n\t\tc.KeyAlgorithm = CertificateKeyAlgorithmType(fmt.Sprintf(\"EC%d\", keySize))\n\tcase x509.Ed25519:\n\t\tc.KeyAlgorithm = CertificateKeyAlgorithmType(\"Ed25519\")\n\tdefault:\n\t\tc.KeyAlgorithm = CertificateKeyAlgorithmType(\"\")\n\t}\n\n\treturn c\n}\n\nfunc (c *Certificate) PopulateFromPEM(certPEM, privkeyPEM string) *Certificate {\n\tc.Certificate = certPEM\n\tc.PrivateKey = privkeyPEM\n\n\t_, issuerCertPEM, _ := xcert.ExtractCertificatesFromPEM(certPEM)\n\tc.IssuerCertificate = issuerCertPEM\n\n\tcertX509, _ := xcert.ParseCertificateFromPEM(certPEM)\n\tif certX509 != nil {\n\t\treturn c.PopulateFromX509(certX509)\n\t}\n\n\treturn c\n}\n\ntype CertificateSourceType string\n\nconst (\n\tCertificateSourceTypeRequest = CertificateSourceType(\"request\")\n\tCertificateSourceTypeUpload  = CertificateSourceType(\"upload\")\n)\n\ntype CertificateKeyAlgorithmType string\n\nconst (\n\tCertificateKeyAlgorithmTypeRSA2048 = CertificateKeyAlgorithmType(\"RSA2048\")\n\tCertificateKeyAlgorithmTypeRSA3072 = CertificateKeyAlgorithmType(\"RSA3072\")\n\tCertificateKeyAlgorithmTypeRSA4096 = CertificateKeyAlgorithmType(\"RSA4096\")\n\tCertificateKeyAlgorithmTypeRSA8192 = CertificateKeyAlgorithmType(\"RSA8192\")\n\tCertificateKeyAlgorithmTypeEC256   = CertificateKeyAlgorithmType(\"EC256\")\n\tCertificateKeyAlgorithmTypeEC384   = CertificateKeyAlgorithmType(\"EC384\")\n\tCertificateKeyAlgorithmTypeEC512   = CertificateKeyAlgorithmType(\"EC512\")\n)\n\nfunc (t CertificateKeyAlgorithmType) KeyType() (certcrypto.KeyType, error) {\n\tkeyTypeMap := map[CertificateKeyAlgorithmType]certcrypto.KeyType{\n\t\tCertificateKeyAlgorithmTypeRSA2048: certcrypto.RSA2048,\n\t\tCertificateKeyAlgorithmTypeRSA3072: certcrypto.RSA3072,\n\t\tCertificateKeyAlgorithmTypeRSA4096: certcrypto.RSA4096,\n\t\tCertificateKeyAlgorithmTypeRSA8192: certcrypto.RSA8192,\n\t\tCertificateKeyAlgorithmTypeEC256:   certcrypto.EC256,\n\t\tCertificateKeyAlgorithmTypeEC384:   certcrypto.EC384,\n\t}\n\n\tif keyType, ok := keyTypeMap[t]; ok {\n\t\treturn keyType, nil\n\t}\n\n\treturn certcrypto.RSA2048, fmt.Errorf(\"unsupported key algorithm type: '%s'\", t)\n}\n\ntype CertificateFormatType string\n\nconst (\n\tCertificateFormatTypePEM CertificateFormatType = \"PEM\"\n\tCertificateFormatTypePFX CertificateFormatType = \"PFX\"\n\tCertificateFormatTypeJKS CertificateFormatType = \"JKS\"\n)\n"
  },
  {
    "path": "internal/domain/dtos/certificate.go",
    "content": "package dtos\n\ntype CertificateDownloadReq struct {\n\tCertificateId     string `json:\"-\"`\n\tCertificateFormat string `json:\"format\"`\n}\n\ntype CertificateDownloadResp struct {\n\tFileBytes  []byte `json:\"fileBytes\"`\n\tFileFormat string `json:\"fileFormat\"`\n}\n\ntype CertificateRevokeReq struct {\n\tCertificateId string `json:\"-\"`\n}\n\ntype CertificateRevokeResp struct{}\n"
  },
  {
    "path": "internal/domain/dtos/notify.go",
    "content": "package dtos\n\ntype NotifyTestPushReq struct {\n\tProvider string `json:\"provider\"`\n\tAccessId string `json:\"accessId\"`\n}\n\ntype NotifyTestPushResp struct{}\n"
  },
  {
    "path": "internal/domain/dtos/workflow.go",
    "content": "package dtos\n\nimport \"github.com/certimate-go/certimate/internal/domain\"\n\ntype WorkflowStartRunReq struct {\n\tWorkflowId string                     `json:\"-\"`\n\tRunTrigger domain.WorkflowTriggerType `json:\"trigger\"`\n}\n\ntype WorkflowStartRunResp struct {\n\tRunId string `json:\"runId\"`\n}\n\ntype WorkflowCancelRunReq struct {\n\tWorkflowId string `json:\"-\"`\n\tRunId      string `json:\"-\"`\n}\n\ntype WorkflowCancelRunResp struct{}\n\ntype WorkflowStatisticsResp struct {\n\tConcurrency      int      `json:\"concurrency\"`\n\tPendingRunIds    []string `json:\"pendingRunIds\"`\n\tProcessingRunIds []string `json:\"processingRunIds\"`\n}\n"
  },
  {
    "path": "internal/domain/error.go",
    "content": "package domain\n\nvar (\n\tErrInvalidParams  = NewError(400, \"invalid params\")\n\tErrRecordNotFound = NewError(404, \"record not found\")\n)\n\ntype Error struct {\n\tCode int    `json:\"code\"`\n\tMsg  string `json:\"msg\"`\n}\n\nfunc NewError(code int, msg string) *Error {\n\tif code == 0 {\n\t\tcode = -1\n\t}\n\n\treturn &Error{code, msg}\n}\n\nfunc (e *Error) Error() string {\n\treturn e.Msg\n}\n\nfunc IsRecordNotFoundError(err error) bool {\n\tif e, ok := err.(*Error); ok {\n\t\treturn e.Code == ErrRecordNotFound.Code\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/domain/expr/expr.go",
    "content": "package expr\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strconv\"\n)\n\ntype (\n\tExprType               string\n\tExprComparisonOperator string\n\tExprLogicalOperator    string\n\tExprValueType          string\n)\n\nconst (\n\tGreaterThan    ExprComparisonOperator = \"gt\"\n\tGreaterOrEqual ExprComparisonOperator = \"gte\"\n\tLessThan       ExprComparisonOperator = \"lt\"\n\tLessOrEqual    ExprComparisonOperator = \"lte\"\n\tEqual          ExprComparisonOperator = \"eq\"\n\tNotEqual       ExprComparisonOperator = \"neq\"\n\n\tAnd ExprLogicalOperator = \"and\"\n\tOr  ExprLogicalOperator = \"or\"\n\tNot ExprLogicalOperator = \"not\"\n\n\tNumber  ExprValueType = \"number\"\n\tString  ExprValueType = \"string\"\n\tBoolean ExprValueType = \"boolean\"\n\n\tConstantExprType   ExprType = \"const\"\n\tVariantExprType    ExprType = \"var\"\n\tComparisonExprType ExprType = \"comparison\"\n\tLogicalExprType    ExprType = \"logical\"\n\tNotExprType        ExprType = \"not\"\n)\n\ntype EvalResult struct {\n\tType  ExprValueType\n\tValue any\n}\n\nfunc (e *EvalResult) GetFloat64() (float64, error) {\n\tif e.Type != Number {\n\t\treturn 0, fmt.Errorf(\"type mismatch: %s\", e.Type)\n\t}\n\n\tstringValue, ok := e.Value.(string)\n\tif !ok {\n\t\treturn 0, fmt.Errorf(\"value is not a string: %v\", e.Value)\n\t}\n\n\tfloatValue, err := strconv.ParseFloat(stringValue, 64)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to parse float64: %w\", err)\n\t}\n\treturn floatValue, nil\n}\n\nfunc (e *EvalResult) GetBool() (bool, error) {\n\tif e.Type != Boolean {\n\t\treturn false, fmt.Errorf(\"type mismatch: %s\", e.Type)\n\t}\n\n\tstrValue, ok := e.Value.(string)\n\tif ok {\n\t\tif strValue == \"true\" {\n\t\t\treturn true, nil\n\t\t} else if strValue == \"false\" {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, fmt.Errorf(\"value is not a boolean: %v\", e.Value)\n\t}\n\n\tboolValue, ok := e.Value.(bool)\n\tif !ok {\n\t\treturn false, fmt.Errorf(\"value is not a boolean: %v\", e.Value)\n\t}\n\n\treturn boolValue, nil\n}\n\nfunc (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) {\n\tif e.Type != other.Type {\n\t\treturn nil, fmt.Errorf(\"type mismatch: %s vs %s\", e.Type, other.Type)\n\t}\n\n\tswitch e.Type {\n\tcase String:\n\t\treturn &EvalResult{\n\t\t\tType:  Boolean,\n\t\t\tValue: e.Value.(string) > other.Value.(string),\n\t\t}, nil\n\n\tcase Number:\n\t\tleft, err := e.GetFloat64()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tright, err := other.GetFloat64()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &EvalResult{\n\t\t\tType:  Boolean,\n\t\t\tValue: left > right,\n\t\t}, nil\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported value type: %s\", e.Type)\n\t}\n}\n\nfunc (e *EvalResult) GreaterOrEqual(other *EvalResult) (*EvalResult, error) {\n\tif e.Type != other.Type {\n\t\treturn nil, fmt.Errorf(\"type mismatch: %s vs %s\", e.Type, other.Type)\n\t}\n\n\tswitch e.Type {\n\tcase String:\n\t\treturn &EvalResult{\n\t\t\tType:  Boolean,\n\t\t\tValue: e.Value.(string) >= other.Value.(string),\n\t\t}, nil\n\n\tcase Number:\n\t\tleft, err := e.GetFloat64()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tright, err := other.GetFloat64()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &EvalResult{\n\t\t\tType:  Boolean,\n\t\t\tValue: left >= right,\n\t\t}, nil\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported value type: %s\", e.Type)\n\t}\n}\n\nfunc (e *EvalResult) LessThan(other *EvalResult) (*EvalResult, error) {\n\tif e.Type != other.Type {\n\t\treturn nil, fmt.Errorf(\"type mismatch: %s vs %s\", e.Type, other.Type)\n\t}\n\n\tswitch e.Type {\n\tcase String:\n\t\treturn &EvalResult{\n\t\t\tType:  Boolean,\n\t\t\tValue: e.Value.(string) < other.Value.(string),\n\t\t}, nil\n\n\tcase Number:\n\t\tleft, err := e.GetFloat64()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tright, err := other.GetFloat64()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &EvalResult{\n\t\t\tType:  Boolean,\n\t\t\tValue: left < right,\n\t\t}, nil\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported value type: %s\", e.Type)\n\t}\n}\n\nfunc (e *EvalResult) LessOrEqual(other *EvalResult) (*EvalResult, error) {\n\tif e.Type != other.Type {\n\t\treturn nil, fmt.Errorf(\"type mismatch: %s vs %s\", e.Type, other.Type)\n\t}\n\n\tswitch e.Type {\n\tcase String:\n\t\treturn &EvalResult{\n\t\t\tType:  Boolean,\n\t\t\tValue: e.Value.(string) <= other.Value.(string),\n\t\t}, nil\n\n\tcase Number:\n\t\tleft, err := e.GetFloat64()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tright, err := other.GetFloat64()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &EvalResult{\n\t\t\tType:  Boolean,\n\t\t\tValue: left <= right,\n\t\t}, nil\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported value type: %s\", e.Type)\n\t}\n}\n\nfunc (e *EvalResult) Equal(other *EvalResult) (*EvalResult, error) {\n\tif e.Type != other.Type {\n\t\treturn nil, fmt.Errorf(\"type mismatch: %s vs %s\", e.Type, other.Type)\n\t}\n\n\tswitch e.Type {\n\tcase String:\n\t\treturn &EvalResult{\n\t\t\tType:  Boolean,\n\t\t\tValue: e.Value.(string) == other.Value.(string),\n\t\t}, nil\n\n\tcase Number:\n\t\tleft, err := e.GetFloat64()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tright, err := other.GetFloat64()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &EvalResult{\n\t\t\tType:  Boolean,\n\t\t\tValue: left == right,\n\t\t}, nil\n\n\tcase Boolean:\n\t\tleft, err := e.GetBool()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tright, err := other.GetBool()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &EvalResult{\n\t\t\tType:  Boolean,\n\t\t\tValue: left == right,\n\t\t}, nil\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported value type: %s\", e.Type)\n\t}\n}\n\nfunc (e *EvalResult) NotEqual(other *EvalResult) (*EvalResult, error) {\n\tif e.Type != other.Type {\n\t\treturn nil, fmt.Errorf(\"type mismatch: %s vs %s\", e.Type, other.Type)\n\t}\n\n\tswitch e.Type {\n\tcase String:\n\t\treturn &EvalResult{\n\t\t\tType:  Boolean,\n\t\t\tValue: e.Value.(string) != other.Value.(string),\n\t\t}, nil\n\n\tcase Number:\n\t\tleft, err := e.GetFloat64()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tright, err := other.GetFloat64()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &EvalResult{\n\t\t\tType:  Boolean,\n\t\t\tValue: left != right,\n\t\t}, nil\n\n\tcase Boolean:\n\t\tleft, err := e.GetBool()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tright, err := other.GetBool()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &EvalResult{\n\t\t\tType:  Boolean,\n\t\t\tValue: left != right,\n\t\t}, nil\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported value type: %s\", e.Type)\n\t}\n}\n\nfunc (e *EvalResult) And(other *EvalResult) (*EvalResult, error) {\n\tif e.Type != other.Type {\n\t\treturn nil, fmt.Errorf(\"type mismatch: %s vs %s\", e.Type, other.Type)\n\t}\n\n\tswitch e.Type {\n\tcase Boolean:\n\t\tleft, err := e.GetBool()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tright, err := other.GetBool()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &EvalResult{\n\t\t\tType:  Boolean,\n\t\t\tValue: left && right,\n\t\t}, nil\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported value type: %s\", e.Type)\n\t}\n}\n\nfunc (e *EvalResult) Or(other *EvalResult) (*EvalResult, error) {\n\tif e.Type != other.Type {\n\t\treturn nil, fmt.Errorf(\"type mismatch: %s vs %s\", e.Type, other.Type)\n\t}\n\n\tswitch e.Type {\n\tcase Boolean:\n\t\tleft, err := e.GetBool()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tright, err := other.GetBool()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &EvalResult{\n\t\t\tType:  Boolean,\n\t\t\tValue: left || right,\n\t\t}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported value type: %s\", e.Type)\n\t}\n}\n\nfunc (e *EvalResult) Not() (*EvalResult, error) {\n\tif e.Type != Boolean {\n\t\treturn nil, fmt.Errorf(\"type mismatch: %s\", e.Type)\n\t}\n\n\tboolValue, err := e.GetBool()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &EvalResult{\n\t\tType:  Boolean,\n\t\tValue: !boolValue,\n\t}, nil\n}\n\ntype Expr interface {\n\tGetType() ExprType\n\tEval(variables map[string]map[string]any) (*EvalResult, error)\n}\n\ntype ExprValueSelector struct {\n\tId   string        `json:\"id\"`\n\tName string        `json:\"name\"`\n\tType ExprValueType `json:\"type\"`\n}\n\ntype ConstantExpr struct {\n\tType      ExprType      `json:\"type\"`\n\tValue     string        `json:\"value\"`\n\tValueType ExprValueType `json:\"valueType\"`\n}\n\nfunc (c ConstantExpr) GetType() ExprType { return c.Type }\n\nfunc (c ConstantExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {\n\treturn &EvalResult{\n\t\tType:  c.ValueType,\n\t\tValue: c.Value,\n\t}, nil\n}\n\ntype VariantExpr struct {\n\tType     ExprType          `json:\"type\"`\n\tSelector ExprValueSelector `json:\"selector\"`\n}\n\nfunc (v VariantExpr) GetType() ExprType { return v.Type }\n\nfunc (v VariantExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {\n\tif v.Selector.Id == \"\" {\n\t\treturn nil, fmt.Errorf(\"node id is empty\")\n\t}\n\tif v.Selector.Name == \"\" {\n\t\treturn nil, fmt.Errorf(\"name is empty\")\n\t}\n\n\tif _, ok := variables[v.Selector.Id]; !ok {\n\t\treturn nil, fmt.Errorf(\"node %s not found\", v.Selector.Id)\n\t}\n\n\tif _, ok := variables[v.Selector.Id][v.Selector.Name]; !ok {\n\t\treturn nil, fmt.Errorf(\"variable %s not found in node %s\", v.Selector.Name, v.Selector.Id)\n\t}\n\treturn &EvalResult{\n\t\tType:  v.Selector.Type,\n\t\tValue: variables[v.Selector.Id][v.Selector.Name],\n\t}, nil\n}\n\ntype ComparisonExpr struct {\n\tType     ExprType               `json:\"type\"` // compare\n\tOperator ExprComparisonOperator `json:\"operator\"`\n\tLeft     Expr                   `json:\"left\"`\n\tRight    Expr                   `json:\"right\"`\n}\n\nfunc (c ComparisonExpr) GetType() ExprType { return c.Type }\n\nfunc (c ComparisonExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {\n\tleft, err := c.Left.Eval(variables)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tright, err := c.Right.Eval(variables)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch c.Operator {\n\tcase GreaterThan:\n\t\treturn left.GreaterThan(right)\n\tcase LessThan:\n\t\treturn left.LessThan(right)\n\tcase GreaterOrEqual:\n\t\treturn left.GreaterOrEqual(right)\n\tcase LessOrEqual:\n\t\treturn left.LessOrEqual(right)\n\tcase Equal:\n\t\treturn left.Equal(right)\n\tcase NotEqual:\n\t\treturn left.NotEqual(right)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown expression operator: %s\", c.Operator)\n\t}\n}\n\ntype LogicalExpr struct {\n\tType     ExprType            `json:\"type\"` // logical\n\tOperator ExprLogicalOperator `json:\"operator\"`\n\tLeft     Expr                `json:\"left\"`\n\tRight    Expr                `json:\"right\"`\n}\n\nfunc (l LogicalExpr) GetType() ExprType { return l.Type }\n\nfunc (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {\n\tleft, err := l.Left.Eval(variables)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tright, err := l.Right.Eval(variables)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch l.Operator {\n\tcase And:\n\t\treturn left.And(right)\n\tcase Or:\n\t\treturn left.Or(right)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown expression operator: %s\", l.Operator)\n\t}\n}\n\ntype NotExpr struct {\n\tType ExprType `json:\"type\"` // not\n\tExpr Expr     `json:\"expr\"`\n}\n\nfunc (n NotExpr) GetType() ExprType { return n.Type }\n\nfunc (n NotExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {\n\tinner, err := n.Expr.Eval(variables)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn inner.Not()\n}\n\ntype rawExpr struct {\n\tType ExprType `json:\"type\"`\n}\n\nfunc MarshalExpr(e Expr) ([]byte, error) {\n\treturn json.Marshal(e)\n}\n\nfunc UnmarshalExpr(data []byte) (Expr, error) {\n\tvar typ rawExpr\n\tif err := json.Unmarshal(data, &typ); err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch typ.Type {\n\tcase ConstantExprType:\n\t\tvar e ConstantExpr\n\t\tif err := json.Unmarshal(data, &e); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn e, nil\n\tcase VariantExprType:\n\t\tvar e VariantExpr\n\t\tif err := json.Unmarshal(data, &e); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn e, nil\n\tcase ComparisonExprType:\n\t\tvar e ComparisonExprRaw\n\t\tif err := json.Unmarshal(data, &e); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn e.ToComparisonExpr()\n\tcase LogicalExprType:\n\t\tvar e LogicalExprRaw\n\t\tif err := json.Unmarshal(data, &e); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn e.ToLogicalExpr()\n\tcase NotExprType:\n\t\tvar e NotExprRaw\n\t\tif err := json.Unmarshal(data, &e); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn e.ToNotExpr()\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown expression type: %s\", typ.Type)\n\t}\n}\n\ntype ComparisonExprRaw struct {\n\tType     ExprType               `json:\"type\"`\n\tOperator ExprComparisonOperator `json:\"operator\"`\n\tLeft     json.RawMessage        `json:\"left\"`\n\tRight    json.RawMessage        `json:\"right\"`\n}\n\nfunc (r ComparisonExprRaw) ToComparisonExpr() (ComparisonExpr, error) {\n\tleftExpr, err := UnmarshalExpr(r.Left)\n\tif err != nil {\n\t\treturn ComparisonExpr{}, err\n\t}\n\trightExpr, err := UnmarshalExpr(r.Right)\n\tif err != nil {\n\t\treturn ComparisonExpr{}, err\n\t}\n\treturn ComparisonExpr{\n\t\tType:     r.Type,\n\t\tOperator: r.Operator,\n\t\tLeft:     leftExpr,\n\t\tRight:    rightExpr,\n\t}, nil\n}\n\ntype LogicalExprRaw struct {\n\tType     ExprType            `json:\"type\"`\n\tOperator ExprLogicalOperator `json:\"operator\"`\n\tLeft     json.RawMessage     `json:\"left\"`\n\tRight    json.RawMessage     `json:\"right\"`\n}\n\nfunc (r LogicalExprRaw) ToLogicalExpr() (LogicalExpr, error) {\n\tleft, err := UnmarshalExpr(r.Left)\n\tif err != nil {\n\t\treturn LogicalExpr{}, err\n\t}\n\tright, err := UnmarshalExpr(r.Right)\n\tif err != nil {\n\t\treturn LogicalExpr{}, err\n\t}\n\treturn LogicalExpr{\n\t\tType:     r.Type,\n\t\tOperator: r.Operator,\n\t\tLeft:     left,\n\t\tRight:    right,\n\t}, nil\n}\n\ntype NotExprRaw struct {\n\tType ExprType        `json:\"type\"`\n\tExpr json.RawMessage `json:\"expr\"`\n}\n\nfunc (r NotExprRaw) ToNotExpr() (NotExpr, error) {\n\tinner, err := UnmarshalExpr(r.Expr)\n\tif err != nil {\n\t\treturn NotExpr{}, err\n\t}\n\treturn NotExpr{\n\t\tType: r.Type,\n\t\tExpr: inner,\n\t}, nil\n}\n"
  },
  {
    "path": "internal/domain/expr/expr_test.go",
    "content": "package expr\n\nimport (\n\t\"testing\"\n)\n\nfunc TestLogicalEval(t *testing.T) {\n\t// 测试逻辑表达式 and\n\tlogicalExpr := LogicalExpr{\n\t\tLeft: ConstantExpr{\n\t\t\tType:      \"const\",\n\t\t\tValue:     \"true\",\n\t\t\tValueType: \"boolean\",\n\t\t},\n\t\tOperator: And,\n\t\tRight: ConstantExpr{\n\t\t\tType:      \"const\",\n\t\t\tValue:     \"true\",\n\t\t\tValueType: \"boolean\",\n\t\t},\n\t}\n\tresult, err := logicalExpr.Eval(nil)\n\tif err != nil {\n\t\tt.Errorf(\"failed to evaluate logical expression: %v\", err)\n\t}\n\tif result.Value != true {\n\t\tt.Errorf(\"expected true, got %v\", result)\n\t}\n\n\t// 测试逻辑表达式 or\n\torExpr := LogicalExpr{\n\t\tLeft: ConstantExpr{\n\t\t\tType:      \"const\",\n\t\t\tValue:     \"true\",\n\t\t\tValueType: \"boolean\",\n\t\t},\n\t\tOperator: Or,\n\t\tRight: ConstantExpr{\n\t\t\tType:      \"const\",\n\t\t\tValue:     \"true\",\n\t\t\tValueType: \"boolean\",\n\t\t},\n\t}\n\tresult, err = orExpr.Eval(nil)\n\tif err != nil {\n\t\tt.Errorf(\"failed to evaluate logical expression: %v\", err)\n\t}\n\tif result.Value != true {\n\t\tt.Errorf(\"expected true, got %v\", result)\n\t}\n}\n\nfunc TestUnmarshalExpr(t *testing.T) {\n\ttype args struct {\n\t\tdata []byte\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    Expr\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"test1\",\n\t\t\targs: args{\n\t\t\t\tdata: []byte(`{\"left\":{\"left\":{\"selector\":{\"id\":\"ODnYSOXB6HQP2_vz6JcZE\",\"name\":\"certificate.validity\",\"type\":\"boolean\"},\"type\":\"var\"},\"operator\":\"is\",\"right\":{\"type\":\"const\",\"value\":true,\"valueType\":\"boolean\"},\"type\":\"comparison\"},\"operator\":\"and\",\"right\":{\"left\":{\"selector\":{\"id\":\"ODnYSOXB6HQP2_vz6JcZE\",\"name\":\"certificate.daysLeft\",\"type\":\"number\"},\"type\":\"var\"},\"operator\":\"eq\",\"right\":{\"type\":\"const\",\"value\":2,\"valueType\":\"number\"},\"type\":\"comparison\"},\"type\":\"logical\"}`),\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := UnmarshalExpr(tt.args.data)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"UnmarshalExpr() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got == nil {\n\t\t\t\tt.Errorf(\"UnmarshalExpr() got = nil, want %v\", tt.want)\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestExpr_Eval(t *testing.T) {\n\ttype args struct {\n\t\tvariables map[string]map[string]any\n\t\tdata      []byte\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *EvalResult\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"test1\",\n\t\t\targs: args{\n\t\t\t\tvariables: map[string]map[string]any{\n\t\t\t\t\t\"ODnYSOXB6HQP2_vz6JcZE\": {\n\t\t\t\t\t\t\"certificate.validity\": true,\n\t\t\t\t\t\t\"certificate.daysLeft\": 2,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdata: []byte(`{\"left\":{\"left\":{\"selector\":{\"id\":\"ODnYSOXB6HQP2_vz6JcZE\",\"name\":\"certificate.validity\",\"type\":\"boolean\"},\"type\":\"var\"},\"operator\":\"is\",\"right\":{\"type\":\"const\",\"value\":true,\"valueType\":\"boolean\"},\"type\":\"comparison\"},\"operator\":\"and\",\"right\":{\"left\":{\"selector\":{\"id\":\"ODnYSOXB6HQP2_vz6JcZE\",\"name\":\"certificate.daysLeft\",\"type\":\"number\"},\"type\":\"var\"},\"operator\":\"eq\",\"right\":{\"type\":\"const\",\"value\":2,\"valueType\":\"number\"},\"type\":\"comparison\"},\"type\":\"logical\"}`),\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := UnmarshalExpr(tt.args.data)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"UnmarshalExpr() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tgot, err := c.Eval(tt.args.variables)\n\t\t\tt.Log(\"got:\", got)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"ConstExpr.Eval() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got.Value != true {\n\t\t\t\tt.Errorf(\"ConstExpr.Eval() got = %v, want %v\", got.Value, true)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/domain/meta.go",
    "content": "package domain\n\nimport \"time\"\n\ntype Meta struct {\n\tId        string    `db:\"id\"      json:\"id\"`\n\tCreatedAt time.Time `db:\"created\" json:\"created\"`\n\tUpdatedAt time.Time `db:\"updated\" json:\"updated\"`\n}\n"
  },
  {
    "path": "internal/domain/provider.go",
    "content": "package domain\n\ntype AccessProviderType string\n\n/*\n授权提供商类型常量值。\n\n注意：如果追加新的常量值，请保持以 ASCII 排序。\nNOTICE: If you add new constant, please keep ASCII order.\n*/\nconst (\n\tAccessProviderType1Panel              = AccessProviderType(\"1panel\")\n\tAccessProviderType35cn                = AccessProviderType(\"35cn\")\n\tAccessProviderType51DNScom            = AccessProviderType(\"51dnscom\")\n\tAccessProviderTypeACMECA              = AccessProviderType(\"acmeca\")\n\tAccessProviderTypeACMEDNS             = AccessProviderType(\"acmedns\")\n\tAccessProviderTypeACMEHttpReq         = AccessProviderType(\"acmehttpreq\")\n\tAccessProviderTypeActalisSSL          = AccessProviderType(\"actalisssl\")\n\tAccessProviderTypeAkamai              = AccessProviderType(\"akamai\")\n\tAccessProviderTypeAliyun              = AccessProviderType(\"aliyun\")\n\tAccessProviderTypeAPISIX              = AccessProviderType(\"apisix\")\n\tAccessProviderTypeArvanCloud          = AccessProviderType(\"arvancloud\")\n\tAccessProviderTypeAWS                 = AccessProviderType(\"aws\")\n\tAccessProviderTypeAzure               = AccessProviderType(\"azure\")\n\tAccessProviderTypeBaiduCloud          = AccessProviderType(\"baiducloud\")\n\tAccessProviderTypeBaishan             = AccessProviderType(\"baishan\")\n\tAccessProviderTypeBaotaPanel          = AccessProviderType(\"baotapanel\")\n\tAccessProviderTypeBaotaPanelGo        = AccessProviderType(\"baotapanelgo\")\n\tAccessProviderTypeBaotaWAF            = AccessProviderType(\"baotawaf\")\n\tAccessProviderTypeBookMyName          = AccessProviderType(\"bookmyname\")\n\tAccessProviderTypeBunny               = AccessProviderType(\"bunny\")\n\tAccessProviderTypeBytePlus            = AccessProviderType(\"byteplus\")\n\tAccessProviderTypeCacheFly            = AccessProviderType(\"cachefly\")\n\tAccessProviderTypeCdnfly              = AccessProviderType(\"cdnfly\")\n\tAccessProviderTypeCloudflare          = AccessProviderType(\"cloudflare\")\n\tAccessProviderTypeClouDNS             = AccessProviderType(\"cloudns\")\n\tAccessProviderTypeCMCCCloud           = AccessProviderType(\"cmcccloud\")\n\tAccessProviderTypeConstellix          = AccessProviderType(\"constellix\")\n\tAccessProviderTypeCPanel              = AccessProviderType(\"cpanel\")\n\tAccessProviderTypeCTCCCloud           = AccessProviderType(\"ctcccloud\")\n\tAccessProviderTypeCUCCCloud           = AccessProviderType(\"cucccloud\") // 联通云（预留）\n\tAccessProviderTypeDeSEC               = AccessProviderType(\"desec\")\n\tAccessProviderTypeDigiCert            = AccessProviderType(\"digicert\")\n\tAccessProviderTypeDigitalOcean        = AccessProviderType(\"digitalocean\")\n\tAccessProviderTypeDingTalkBot         = AccessProviderType(\"dingtalkbot\")\n\tAccessProviderTypeDiscordBot          = AccessProviderType(\"discordbot\")\n\tAccessProviderTypeDNSExit             = AccessProviderType(\"dnsexit\")\n\tAccessProviderTypeDNSLA               = AccessProviderType(\"dnsla\")\n\tAccessProviderTypeDNSMadeEasy         = AccessProviderType(\"dnsmadeeasy\")\n\tAccessProviderTypeDogeCloud           = AccessProviderType(\"dogecloud\")\n\tAccessProviderTypeDokploy             = AccessProviderType(\"dokploy\")\n\tAccessProviderTypeDuckDNS             = AccessProviderType(\"duckdns\")\n\tAccessProviderTypeDynu                = AccessProviderType(\"dynu\")\n\tAccessProviderTypeDynv6               = AccessProviderType(\"dynv6\")\n\tAccessProviderTypeEmail               = AccessProviderType(\"email\")\n\tAccessProviderTypeFastly              = AccessProviderType(\"fastly\") // Fastly（预留）\n\tAccessProviderTypeFlexCDN             = AccessProviderType(\"flexcdn\")\n\tAccessProviderTypeFlyIO               = AccessProviderType(\"flyio\")\n\tAccessProviderTypeGandinet            = AccessProviderType(\"gandinet\")\n\tAccessProviderTypeGcore               = AccessProviderType(\"gcore\")\n\tAccessProviderTypeGlobalSignAtlas     = AccessProviderType(\"globalsignatlas\")\n\tAccessProviderTypeGname               = AccessProviderType(\"gname\")\n\tAccessProviderTypeGoDaddy             = AccessProviderType(\"godaddy\")\n\tAccessProviderTypeGoEdge              = AccessProviderType(\"goedge\")\n\tAccessProviderTypeGoogleTrustServices = AccessProviderType(\"googletrustservices\")\n\tAccessProviderTypeHetzner             = AccessProviderType(\"hetzner\")\n\tAccessProviderTypeHostingde           = AccessProviderType(\"hostingde\")\n\tAccessProviderTypeHostinger           = AccessProviderType(\"hostinger\")\n\tAccessProviderTypeHuaweiCloud         = AccessProviderType(\"huaweicloud\")\n\tAccessProviderTypeInfomaniak          = AccessProviderType(\"infomaniak\")\n\tAccessProviderTypeIONOS               = AccessProviderType(\"ionos\")\n\tAccessProviderTypeJDCloud             = AccessProviderType(\"jdcloud\")\n\tAccessProviderTypeKong                = AccessProviderType(\"kong\")\n\tAccessProviderTypeKsyun               = AccessProviderType(\"ksyun\")\n\tAccessProviderTypeKubernetes          = AccessProviderType(\"k8s\")\n\tAccessProviderTypeLarkBot             = AccessProviderType(\"larkbot\")\n\tAccessProviderTypeLeCDN               = AccessProviderType(\"lecdn\")\n\tAccessProviderTypeLetsEncrypt         = AccessProviderType(\"letsencrypt\")\n\tAccessProviderTypeLetsEncryptStaging  = AccessProviderType(\"letsencryptstaging\")\n\tAccessProviderTypeLinode              = AccessProviderType(\"linode\")\n\tAccessProviderTypeLiteSSL             = AccessProviderType(\"litessl\")\n\tAccessProviderTypeLocal               = AccessProviderType(\"local\")\n\tAccessProviderTypeMattermost          = AccessProviderType(\"mattermost\")\n\tAccessProviderTypeMohua               = AccessProviderType(\"mohua\")\n\tAccessProviderTypeNamecheap           = AccessProviderType(\"namecheap\")\n\tAccessProviderTypeNameDotCom          = AccessProviderType(\"namedotcom\")\n\tAccessProviderTypeNameSilo            = AccessProviderType(\"namesilo\")\n\tAccessProviderTypeNetcup              = AccessProviderType(\"netcup\")\n\tAccessProviderTypeNetlify             = AccessProviderType(\"netlify\")\n\tAccessProviderTypeNginxProxyManager   = AccessProviderType(\"nginxproxymanager\")\n\tAccessProviderTypeNS1                 = AccessProviderType(\"ns1\")\n\tAccessProviderTypeOVHcloud            = AccessProviderType(\"ovhcloud\")\n\tAccessProviderTypePorkbun             = AccessProviderType(\"porkbun\")\n\tAccessProviderTypePowerDNS            = AccessProviderType(\"powerdns\")\n\tAccessProviderTypeProxmoxVE           = AccessProviderType(\"proxmoxve\")\n\tAccessProviderTypeQiniu               = AccessProviderType(\"qiniu\")\n\tAccessProviderTypeQingCloud           = AccessProviderType(\"qingcloud\")\n\tAccessProviderTypeRainYun             = AccessProviderType(\"rainyun\")\n\tAccessProviderTypeRatPanel            = AccessProviderType(\"ratpanel\")\n\tAccessProviderTypeRFC2136             = AccessProviderType(\"rfc2136\")\n\tAccessProviderTypeS3                  = AccessProviderType(\"s3\")\n\tAccessProviderTypeSafeLine            = AccessProviderType(\"safeline\")\n\tAccessProviderTypeSectigo             = AccessProviderType(\"sectigo\")\n\tAccessProviderTypeSlackBot            = AccessProviderType(\"slackbot\")\n\tAccessProviderTypeSpaceship           = AccessProviderType(\"spaceship\")\n\tAccessProviderTypeSSH                 = AccessProviderType(\"ssh\")\n\tAccessProviderTypeSSLCOM              = AccessProviderType(\"sslcom\")\n\tAccessProviderTypeSynologyDSM         = AccessProviderType(\"synologydsm\")\n\tAccessProviderTypeTechnitiumDNS       = AccessProviderType(\"technitiumdns\")\n\tAccessProviderTypeTelegramBot         = AccessProviderType(\"telegrambot\")\n\tAccessProviderTypeTencentCloud        = AccessProviderType(\"tencentcloud\")\n\tAccessProviderTypeTodayNIC            = AccessProviderType(\"todaynic\")\n\tAccessProviderTypeUCloud              = AccessProviderType(\"ucloud\")\n\tAccessProviderTypeUniCloud            = AccessProviderType(\"unicloud\")\n\tAccessProviderTypeUpyun               = AccessProviderType(\"upyun\")\n\tAccessProviderTypeVercel              = AccessProviderType(\"vercel\")\n\tAccessProviderTypeVolcEngine          = AccessProviderType(\"volcengine\")\n\tAccessProviderTypeVultr               = AccessProviderType(\"vultr\")\n\tAccessProviderTypeWangsu              = AccessProviderType(\"wangsu\")\n\tAccessProviderTypeWebhook             = AccessProviderType(\"webhook\")\n\tAccessProviderTypeWeComBot            = AccessProviderType(\"wecombot\")\n\tAccessProviderTypeWestcn              = AccessProviderType(\"westcn\")\n\tAccessProviderTypeXinnet              = AccessProviderType(\"xinnet\")\n\tAccessProviderTypeZeroSSL             = AccessProviderType(\"zerossl\")\n)\n\ntype CAProviderType string\n\n/*\n证书颁发机构提供商常量值。\n短横线前的部分始终等于授权提供商类型。\n\n注意：如果追加新的常量值，请保持以 ASCII 排序。\nNOTICE: If you add new constant, please keep ASCII order.\n*/\nconst (\n\tCAProviderTypeACMECA              = CAProviderType(AccessProviderTypeACMECA)\n\tCAProviderTypeActalisSSL          = CAProviderType(AccessProviderTypeActalisSSL)\n\tCAProviderTypeDigiCert            = CAProviderType(AccessProviderTypeDigiCert)\n\tCAProviderTypeGlobalSignAtlas     = CAProviderType(AccessProviderTypeGlobalSignAtlas)\n\tCAProviderTypeGoogleTrustServices = CAProviderType(AccessProviderTypeGoogleTrustServices)\n\tCAProviderTypeLetsEncrypt         = CAProviderType(AccessProviderTypeLetsEncrypt)\n\tCAProviderTypeLetsEncryptStaging  = CAProviderType(AccessProviderTypeLetsEncryptStaging)\n\tCAProviderTypeLiteSSL             = CAProviderType(AccessProviderTypeLiteSSL)\n\tCAProviderTypeSectigo             = CAProviderType(AccessProviderTypeSectigo)\n\tCAProviderTypeSSLCom              = CAProviderType(AccessProviderTypeSSLCOM)\n\tCAProviderTypeZeroSSL             = CAProviderType(AccessProviderTypeZeroSSL)\n)\n\ntype ACMEDns01ProviderType string\n\n/*\nACME DNS-01 提供商常量值。\n短横线前的部分始终等于授权提供商类型。\n\n注意：如果追加新的常量值，请保持以 ASCII 排序。\nNOTICE: If you add new constant, please keep ASCII order.\n*/\nconst (\n\tACMEDns01ProviderType35cn              = ACMEDns01ProviderType(AccessProviderType35cn)\n\tACMEDns01ProviderType51DNScom          = ACMEDns01ProviderType(AccessProviderType51DNScom)\n\tACMEDns01ProviderTypeACMEDNS           = ACMEDns01ProviderType(AccessProviderTypeACMEDNS)\n\tACMEDns01ProviderTypeACMEHttpReq       = ACMEDns01ProviderType(AccessProviderTypeACMEHttpReq)\n\tACMEDns01ProviderTypeAkamai            = ACMEDns01ProviderType(AccessProviderTypeAkamai) // 兼容旧值，等同于 [ACMEDns01ProviderTypeAkamaiEdgeDNS]\n\tACMEDns01ProviderTypeAkamaiEdgeDNS     = ACMEDns01ProviderType(AccessProviderTypeAkamai + \"-edgedns\")\n\tACMEDns01ProviderTypeAliyun            = ACMEDns01ProviderType(AccessProviderTypeAliyun) // 兼容旧值，等同于 [ACMEDns01ProviderTypeAliyunDNS]\n\tACMEDns01ProviderTypeAliyunDNS         = ACMEDns01ProviderType(AccessProviderTypeAliyun + \"-dns\")\n\tACMEDns01ProviderTypeAliyunESA         = ACMEDns01ProviderType(AccessProviderTypeAliyun + \"-esa\")\n\tACMEDns01ProviderTypeArvanCloud        = ACMEDns01ProviderType(AccessProviderTypeArvanCloud)\n\tACMEDns01ProviderTypeAWS               = ACMEDns01ProviderType(AccessProviderTypeAWS) // 兼容旧值，等同于 [ACMEDns01ProviderTypeAWSRoute53]\n\tACMEDns01ProviderTypeAWSRoute53        = ACMEDns01ProviderType(AccessProviderTypeAWS + \"-route53\")\n\tACMEDns01ProviderTypeAzure             = ACMEDns01ProviderType(AccessProviderTypeAzure) // 兼容旧值，等同于 [ACMEDns01ProviderTypeAzure]\n\tACMEDns01ProviderTypeAzureDNS          = ACMEDns01ProviderType(AccessProviderTypeAzure + \"-dns\")\n\tACMEDns01ProviderTypeBaiduCloud        = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud) // 兼容旧值，等同于 [ACMEDns01ProviderTypeBaiduCloudDNS]\n\tACMEDns01ProviderTypeBaiduCloudDNS     = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud + \"-dns\")\n\tACMEDns01ProviderTypeBookMyName        = ACMEDns01ProviderType(AccessProviderTypeBookMyName)\n\tACMEDns01ProviderTypeBunny             = ACMEDns01ProviderType(AccessProviderTypeBunny)\n\tACMEDns01ProviderTypeCloudflare        = ACMEDns01ProviderType(AccessProviderTypeCloudflare)\n\tACMEDns01ProviderTypeClouDNS           = ACMEDns01ProviderType(AccessProviderTypeClouDNS)\n\tACMEDns01ProviderTypeCMCCCloud         = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud) // 兼容旧值，等同于 [ACMEDns01ProviderTypeCMCCCloudDNS]\n\tACMEDns01ProviderTypeCMCCCloudDNS      = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud + \"-dns\")\n\tACMEDns01ProviderTypeConstellix        = ACMEDns01ProviderType(AccessProviderTypeConstellix)\n\tACMEDns01ProviderTypeCPanel            = ACMEDns01ProviderType(AccessProviderTypeCPanel)\n\tACMEDns01ProviderTypeCTCCCloud         = ACMEDns01ProviderType(AccessProviderTypeCTCCCloud) // 兼容旧值，等同于 [ACMEDns01ProviderTypeCTCCCloudSmartDNS]\n\tACMEDns01ProviderTypeCTCCCloudSmartDNS = ACMEDns01ProviderType(AccessProviderTypeCTCCCloud + \"-smartdns\")\n\tACMEDns01ProviderTypeDeSEC             = ACMEDns01ProviderType(AccessProviderTypeDeSEC)\n\tACMEDns01ProviderTypeDigitalOcean      = ACMEDns01ProviderType(AccessProviderTypeDigitalOcean)\n\tACMEDns01ProviderTypeDNSExit           = ACMEDns01ProviderType(AccessProviderTypeDNSExit)\n\tACMEDns01ProviderTypeDNSLA             = ACMEDns01ProviderType(AccessProviderTypeDNSLA)\n\tACMEDns01ProviderTypeDNSMadeEasy       = ACMEDns01ProviderType(AccessProviderTypeDNSMadeEasy)\n\tACMEDns01ProviderTypeDuckDNS           = ACMEDns01ProviderType(AccessProviderTypeDuckDNS)\n\tACMEDns01ProviderTypeDynu              = ACMEDns01ProviderType(AccessProviderTypeDynu)\n\tACMEDns01ProviderTypeDynv6             = ACMEDns01ProviderType(AccessProviderTypeDynv6)\n\tACMEDns01ProviderTypeGandinet          = ACMEDns01ProviderType(AccessProviderTypeGandinet)\n\tACMEDns01ProviderTypeGcore             = ACMEDns01ProviderType(AccessProviderTypeGcore)\n\tACMEDns01ProviderTypeGname             = ACMEDns01ProviderType(AccessProviderTypeGname)\n\tACMEDns01ProviderTypeGoDaddy           = ACMEDns01ProviderType(AccessProviderTypeGoDaddy)\n\tACMEDns01ProviderTypeHetzner           = ACMEDns01ProviderType(AccessProviderTypeHetzner)\n\tACMEDns01ProviderTypeHostingde         = ACMEDns01ProviderType(AccessProviderTypeHostingde)\n\tACMEDns01ProviderTypeHostinger         = ACMEDns01ProviderType(AccessProviderTypeHostinger)\n\tACMEDns01ProviderTypeHuaweiCloud       = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud) // 兼容旧值，等同于 [ACMEDns01ProviderTypeHuaweiCloudDNS]\n\tACMEDns01ProviderTypeHuaweiCloudDNS    = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud + \"-dns\")\n\tACMEDns01ProviderTypeInfomaniak        = ACMEDns01ProviderType(AccessProviderTypeInfomaniak)\n\tACMEDns01ProviderTypeIONOS             = ACMEDns01ProviderType(AccessProviderTypeIONOS)\n\tACMEDns01ProviderTypeJDCloud           = ACMEDns01ProviderType(AccessProviderTypeJDCloud) // 兼容旧值，等同于 [ACMEDns01ProviderTypeJDCloudDNS]\n\tACMEDns01ProviderTypeJDCloudDNS        = ACMEDns01ProviderType(AccessProviderTypeJDCloud + \"-dns\")\n\tACMEDns01ProviderTypeLinode            = ACMEDns01ProviderType(AccessProviderTypeLinode)\n\tACMEDns01ProviderTypeNamecheap         = ACMEDns01ProviderType(AccessProviderTypeNamecheap)\n\tACMEDns01ProviderTypeNameDotCom        = ACMEDns01ProviderType(AccessProviderTypeNameDotCom)\n\tACMEDns01ProviderTypeNameSilo          = ACMEDns01ProviderType(AccessProviderTypeNameSilo)\n\tACMEDns01ProviderTypeNetcup            = ACMEDns01ProviderType(AccessProviderTypeNetcup)\n\tACMEDns01ProviderTypeNetlify           = ACMEDns01ProviderType(AccessProviderTypeNetlify)\n\tACMEDns01ProviderTypeNS1               = ACMEDns01ProviderType(AccessProviderTypeNS1)\n\tACMEDns01ProviderTypeOVHcloud          = ACMEDns01ProviderType(AccessProviderTypeOVHcloud)\n\tACMEDns01ProviderTypePorkbun           = ACMEDns01ProviderType(AccessProviderTypePorkbun)\n\tACMEDns01ProviderTypePowerDNS          = ACMEDns01ProviderType(AccessProviderTypePowerDNS)\n\tACMEDns01ProviderTypeQingCloud         = ACMEDns01ProviderType(AccessProviderTypeQingCloud) // 兼容旧值，等同于 [ACMEDns01ProviderTypeQingCloudDNS]\n\tACMEDns01ProviderTypeQingCloudDNS      = ACMEDns01ProviderType(AccessProviderTypeQingCloud + \"-dns\")\n\tACMEDns01ProviderTypeRainYun           = ACMEDns01ProviderType(AccessProviderTypeRainYun)\n\tACMEDns01ProviderTypeRFC2136           = ACMEDns01ProviderType(AccessProviderTypeRFC2136)\n\tACMEDns01ProviderTypeSpaceship         = ACMEDns01ProviderType(AccessProviderTypeSpaceship)\n\tACMEDns01ProviderTypeTechnitiumDNS     = ACMEDns01ProviderType(AccessProviderTypeTechnitiumDNS)\n\tACMEDns01ProviderTypeTencentCloud      = ACMEDns01ProviderType(AccessProviderTypeTencentCloud) // 兼容旧值，等同于 [ACMEDns01ProviderTypeTencentCloudDNS]\n\tACMEDns01ProviderTypeTencentCloudDNS   = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + \"-dns\")\n\tACMEDns01ProviderTypeTencentCloudEO    = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + \"-eo\")\n\tACMEDns01ProviderTypeTodayNIC          = ACMEDns01ProviderType(AccessProviderTypeTodayNIC)\n\tACMEDns01ProviderTypeUCloud            = ACMEDns01ProviderType(AccessProviderTypeUCloud) // 兼容旧值，等同于 [ACMEDns01ProviderTypeUCloudUDNR]\n\tACMEDns01ProviderTypeUCloudUDNR        = ACMEDns01ProviderType(AccessProviderTypeUCloud + \"-udnr\")\n\tACMEDns01ProviderTypeVercel            = ACMEDns01ProviderType(AccessProviderTypeVercel)\n\tACMEDns01ProviderTypeVolcEngine        = ACMEDns01ProviderType(AccessProviderTypeVolcEngine) // 兼容旧值，等同于 [ACMEDns01ProviderTypeVolcEngineDNS]\n\tACMEDns01ProviderTypeVolcEngineDNS     = ACMEDns01ProviderType(AccessProviderTypeVolcEngine + \"-dns\")\n\tACMEDns01ProviderTypeVultr             = ACMEDns01ProviderType(AccessProviderTypeVultr)\n\tACMEDns01ProviderTypeWestcn            = ACMEDns01ProviderType(AccessProviderTypeWestcn)\n\tACMEDns01ProviderTypeXinnet            = ACMEDns01ProviderType(AccessProviderTypeXinnet)\n)\n\ntype ACMEHttp01ProviderType string\n\n/*\nACME HTTP-01 提供商常量值。\n短横线前的部分始终等于授权提供商类型。\n\n注意：如果追加新的常量值，请保持以 ASCII 排序。\nNOTICE: If you add new constant, please keep ASCII order.\n*/\nconst (\n\tACMEHttp01ProviderTypeLocal = ACMEHttp01ProviderType(AccessProviderTypeLocal)\n\tACMEHttp01ProviderTypeS3    = ACMEHttp01ProviderType(AccessProviderTypeS3)\n\tACMEHttp01ProviderTypeSSH   = ACMEHttp01ProviderType(AccessProviderTypeSSH)\n)\n\ntype DeploymentProviderType string\n\n/*\n部署证书主机提供商常量值。\n短横线前的部分始终等于授权提供商类型。\n\n注意：如果追加新的常量值，请保持以 ASCII 排序。\nNOTICE: If you add new constant, please keep ASCII order.\n*/\nconst (\n\tDeploymentProviderType1Panel                = DeploymentProviderType(AccessProviderType1Panel)\n\tDeploymentProviderType1PanelConsole         = DeploymentProviderType(AccessProviderType1Panel + \"-console\")\n\tDeploymentProviderTypeAliyunALB             = DeploymentProviderType(AccessProviderTypeAliyun + \"-alb\")\n\tDeploymentProviderTypeAliyunAPIGW           = DeploymentProviderType(AccessProviderTypeAliyun + \"-apigw\")\n\tDeploymentProviderTypeAliyunCAS             = DeploymentProviderType(AccessProviderTypeAliyun + \"-cas\")\n\tDeploymentProviderTypeAliyunCASDeploy       = DeploymentProviderType(AccessProviderTypeAliyun + \"-casdeploy\")\n\tDeploymentProviderTypeAliyunCDN             = DeploymentProviderType(AccessProviderTypeAliyun + \"-cdn\")\n\tDeploymentProviderTypeAliyunCLB             = DeploymentProviderType(AccessProviderTypeAliyun + \"-clb\")\n\tDeploymentProviderTypeAliyunDCDN            = DeploymentProviderType(AccessProviderTypeAliyun + \"-dcdn\")\n\tDeploymentProviderTypeAliyunDDoSPro         = DeploymentProviderType(AccessProviderTypeAliyun + \"-ddospro\")\n\tDeploymentProviderTypeAliyunESA             = DeploymentProviderType(AccessProviderTypeAliyun + \"-esa\")\n\tDeploymentProviderTypeAliyunESASaaS         = DeploymentProviderType(AccessProviderTypeAliyun + \"-esasaas\")\n\tDeploymentProviderTypeAliyunFC              = DeploymentProviderType(AccessProviderTypeAliyun + \"-fc\")\n\tDeploymentProviderTypeAliyunGA              = DeploymentProviderType(AccessProviderTypeAliyun + \"-ga\")\n\tDeploymentProviderTypeAliyunLive            = DeploymentProviderType(AccessProviderTypeAliyun + \"-live\")\n\tDeploymentProviderTypeAliyunNLB             = DeploymentProviderType(AccessProviderTypeAliyun + \"-nlb\")\n\tDeploymentProviderTypeAliyunOSS             = DeploymentProviderType(AccessProviderTypeAliyun + \"-oss\")\n\tDeploymentProviderTypeAliyunVOD             = DeploymentProviderType(AccessProviderTypeAliyun + \"-vod\")\n\tDeploymentProviderTypeAliyunWAF             = DeploymentProviderType(AccessProviderTypeAliyun + \"-waf\")\n\tDeploymentProviderTypeAPISIX                = DeploymentProviderType(AccessProviderTypeAPISIX)\n\tDeploymentProviderTypeAWSACM                = DeploymentProviderType(AccessProviderTypeAWS + \"-acm\")\n\tDeploymentProviderTypeAWSCloudFront         = DeploymentProviderType(AccessProviderTypeAWS + \"-cloudfront\")\n\tDeploymentProviderTypeAWSIAM                = DeploymentProviderType(AccessProviderTypeAWS + \"-iam\")\n\tDeploymentProviderTypeAzureKeyVault         = DeploymentProviderType(AccessProviderTypeAzure + \"-keyvault\")\n\tDeploymentProviderTypeBaiduCloudAppBLB      = DeploymentProviderType(AccessProviderTypeBaiduCloud + \"-appblb\")\n\tDeploymentProviderTypeBaiduCloudBLB         = DeploymentProviderType(AccessProviderTypeBaiduCloud + \"-blb\")\n\tDeploymentProviderTypeBaiduCloudCDN         = DeploymentProviderType(AccessProviderTypeBaiduCloud + \"-cdn\")\n\tDeploymentProviderTypeBaiduCloudCert        = DeploymentProviderType(AccessProviderTypeBaiduCloud + \"-cert\")\n\tDeploymentProviderTypeBaishanCDN            = DeploymentProviderType(AccessProviderTypeBaishan + \"-cdn\")\n\tDeploymentProviderTypeBaotaPanel            = DeploymentProviderType(AccessProviderTypeBaotaPanel)\n\tDeploymentProviderTypeBaotaPanelConsole     = DeploymentProviderType(AccessProviderTypeBaotaPanel + \"-console\")\n\tDeploymentProviderTypeBaotaPanelGo          = DeploymentProviderType(AccessProviderTypeBaotaPanelGo)\n\tDeploymentProviderTypeBaotaPanelGoConsole   = DeploymentProviderType(AccessProviderTypeBaotaPanelGo + \"-console\")\n\tDeploymentProviderTypeBaotaWAF              = DeploymentProviderType(AccessProviderTypeBaotaWAF)\n\tDeploymentProviderTypeBaotaWAFConsole       = DeploymentProviderType(AccessProviderTypeBaotaWAF + \"-console\")\n\tDeploymentProviderTypeBunnyCDN              = DeploymentProviderType(AccessProviderTypeBunny + \"-cdn\")\n\tDeploymentProviderTypeBytePlusCDN           = DeploymentProviderType(AccessProviderTypeBytePlus + \"-cdn\")\n\tDeploymentProviderTypeCacheFly              = DeploymentProviderType(AccessProviderTypeCacheFly)\n\tDeploymentProviderTypeCdnfly                = DeploymentProviderType(AccessProviderTypeCdnfly)\n\tDeploymentProviderTypeCPanel                = DeploymentProviderType(AccessProviderTypeCPanel)\n\tDeploymentProviderTypeCTCCCloudAO           = DeploymentProviderType(AccessProviderTypeCTCCCloud + \"-ao\")\n\tDeploymentProviderTypeCTCCCloudCDN          = DeploymentProviderType(AccessProviderTypeCTCCCloud + \"-cdn\")\n\tDeploymentProviderTypeCTCCCloudCMS          = DeploymentProviderType(AccessProviderTypeCTCCCloud + \"-cms\")\n\tDeploymentProviderTypeCTCCCloudELB          = DeploymentProviderType(AccessProviderTypeCTCCCloud + \"-elb\")\n\tDeploymentProviderTypeCTCCCloudFaaS         = DeploymentProviderType(AccessProviderTypeCTCCCloud + \"-faas\")\n\tDeploymentProviderTypeCTCCCloudICDN         = DeploymentProviderType(AccessProviderTypeCTCCCloud + \"-icdn\")\n\tDeploymentProviderTypeCTCCCloudLVDN         = DeploymentProviderType(AccessProviderTypeCTCCCloud + \"-ldvn\")\n\tDeploymentProviderTypeDogeCloudCDN          = DeploymentProviderType(AccessProviderTypeDogeCloud + \"-cdn\")\n\tDeploymentProviderTypeDokploy               = DeploymentProviderType(AccessProviderTypeDokploy)\n\tDeploymentProviderTypeFlexCDN               = DeploymentProviderType(AccessProviderTypeFlexCDN)\n\tDeploymentProviderTypeFlyIO                 = DeploymentProviderType(AccessProviderTypeFlyIO)\n\tDeploymentProviderTypeGcoreCDN              = DeploymentProviderType(AccessProviderTypeGcore + \"-cdn\")\n\tDeploymentProviderTypeGoEdge                = DeploymentProviderType(AccessProviderTypeGoEdge)\n\tDeploymentProviderTypeHuaweiCloudCDN        = DeploymentProviderType(AccessProviderTypeHuaweiCloud + \"-cdn\")\n\tDeploymentProviderTypeHuaweiCloudELB        = DeploymentProviderType(AccessProviderTypeHuaweiCloud + \"-elb\")\n\tDeploymentProviderTypeHuaweiCloudSCM        = DeploymentProviderType(AccessProviderTypeHuaweiCloud + \"-scm\")\n\tDeploymentProviderTypeHuaweiCloudOBS        = DeploymentProviderType(AccessProviderTypeHuaweiCloud + \"-obs\")\n\tDeploymentProviderTypeHuaweiCloudWAF        = DeploymentProviderType(AccessProviderTypeHuaweiCloud + \"-waf\")\n\tDeploymentProviderTypeJDCloudALB            = DeploymentProviderType(AccessProviderTypeJDCloud + \"-alb\")\n\tDeploymentProviderTypeJDCloudCDN            = DeploymentProviderType(AccessProviderTypeJDCloud + \"-cdn\")\n\tDeploymentProviderTypeJDCloudLive           = DeploymentProviderType(AccessProviderTypeJDCloud + \"-live\")\n\tDeploymentProviderTypeJDCloudVOD            = DeploymentProviderType(AccessProviderTypeJDCloud + \"-vod\")\n\tDeploymentProviderTypeKong                  = DeploymentProviderType(AccessProviderTypeKong)\n\tDeploymentProviderTypeKubernetesSecret      = DeploymentProviderType(AccessProviderTypeKubernetes + \"-secret\")\n\tDeploymentProviderTypeKsyunCDN              = DeploymentProviderType(AccessProviderTypeKsyun + \"-cdn\")\n\tDeploymentProviderTypeLeCDN                 = DeploymentProviderType(AccessProviderTypeLeCDN)\n\tDeploymentProviderTypeLocal                 = DeploymentProviderType(AccessProviderTypeLocal)\n\tDeploymentProviderTypeMohuaMVH              = DeploymentProviderType(AccessProviderTypeMohua + \"-mvh\")\n\tDeploymentProviderTypeNetlify               = DeploymentProviderType(AccessProviderTypeNetlify)\n\tDeploymentProviderTypeNginxProxyManager     = DeploymentProviderType(AccessProviderTypeNginxProxyManager)\n\tDeploymentProviderTypeProxmoxVE             = DeploymentProviderType(AccessProviderTypeProxmoxVE)\n\tDeploymentProviderTypeQiniuCDN              = DeploymentProviderType(AccessProviderTypeQiniu + \"-cdn\")\n\tDeploymentProviderTypeQiniuKodo             = DeploymentProviderType(AccessProviderTypeQiniu + \"-kodo\")\n\tDeploymentProviderTypeQiniuPili             = DeploymentProviderType(AccessProviderTypeQiniu + \"-pili\")\n\tDeploymentProviderTypeRainYunRCDN           = DeploymentProviderType(AccessProviderTypeRainYun + \"-rcdn\")\n\tDeploymentProviderTypeRainYunSSLCenter      = DeploymentProviderType(AccessProviderTypeRainYun + \"-sslcenter\")\n\tDeploymentProviderTypeRatPanel              = DeploymentProviderType(AccessProviderTypeRatPanel)\n\tDeploymentProviderTypeRatPanelConsole       = DeploymentProviderType(AccessProviderTypeRatPanel + \"-console\")\n\tDeploymentProviderTypeS3                    = DeploymentProviderType(AccessProviderTypeS3)\n\tDeploymentProviderTypeSafeLine              = DeploymentProviderType(AccessProviderTypeSafeLine)\n\tDeploymentProviderTypeSSH                   = DeploymentProviderType(AccessProviderTypeSSH)\n\tDeploymentProviderTypeSynologyDSM           = DeploymentProviderType(AccessProviderTypeSynologyDSM)\n\tDeploymentProviderTypeTencentCloudCDN       = DeploymentProviderType(AccessProviderTypeTencentCloud + \"-cdn\")\n\tDeploymentProviderTypeTencentCloudCLB       = DeploymentProviderType(AccessProviderTypeTencentCloud + \"-clb\")\n\tDeploymentProviderTypeTencentCloudCOS       = DeploymentProviderType(AccessProviderTypeTencentCloud + \"-cos\")\n\tDeploymentProviderTypeTencentCloudCSS       = DeploymentProviderType(AccessProviderTypeTencentCloud + \"-css\")\n\tDeploymentProviderTypeTencentCloudECDN      = DeploymentProviderType(AccessProviderTypeTencentCloud + \"-ecdn\")\n\tDeploymentProviderTypeTencentCloudEO        = DeploymentProviderType(AccessProviderTypeTencentCloud + \"-eo\")\n\tDeploymentProviderTypeTencentCloudGAAP      = DeploymentProviderType(AccessProviderTypeTencentCloud + \"-gaap\")\n\tDeploymentProviderTypeTencentCloudSCF       = DeploymentProviderType(AccessProviderTypeTencentCloud + \"-scf\")\n\tDeploymentProviderTypeTencentCloudSSL       = DeploymentProviderType(AccessProviderTypeTencentCloud + \"-ssl\")\n\tDeploymentProviderTypeTencentCloudSSLDeploy = DeploymentProviderType(AccessProviderTypeTencentCloud + \"-ssldeploy\")\n\tDeploymentProviderTypeTencentCloudSSLUpdate = DeploymentProviderType(AccessProviderTypeTencentCloud + \"-sslupdate\")\n\tDeploymentProviderTypeTencentCloudVOD       = DeploymentProviderType(AccessProviderTypeTencentCloud + \"-vod\")\n\tDeploymentProviderTypeTencentCloudWAF       = DeploymentProviderType(AccessProviderTypeTencentCloud + \"-waf\")\n\tDeploymentProviderTypeUCloudUALB            = DeploymentProviderType(AccessProviderTypeUCloud + \"-ualb\")\n\tDeploymentProviderTypeUCloudUCDN            = DeploymentProviderType(AccessProviderTypeUCloud + \"-ucdn\")\n\tDeploymentProviderTypeUCloudUCLB            = DeploymentProviderType(AccessProviderTypeUCloud + \"-uclb\")\n\tDeploymentProviderTypeUCloudUEWAF           = DeploymentProviderType(AccessProviderTypeUCloud + \"-uewaf\")\n\tDeploymentProviderTypeUCloudUPathX          = DeploymentProviderType(AccessProviderTypeUCloud + \"-pathx\")\n\tDeploymentProviderTypeUCloudUS3             = DeploymentProviderType(AccessProviderTypeUCloud + \"-us3\")\n\tDeploymentProviderTypeUniCloudWebHost       = DeploymentProviderType(AccessProviderTypeUniCloud + \"-webhost\")\n\tDeploymentProviderTypeUpyunCDN              = DeploymentProviderType(AccessProviderTypeUpyun + \"-cdn\")\n\tDeploymentProviderTypeUpyunFile             = DeploymentProviderType(AccessProviderTypeUpyun + \"-file\")\n\tDeploymentProviderTypeVolcEngineALB         = DeploymentProviderType(AccessProviderTypeVolcEngine + \"-alb\")\n\tDeploymentProviderTypeVolcEngineCDN         = DeploymentProviderType(AccessProviderTypeVolcEngine + \"-cdn\")\n\tDeploymentProviderTypeVolcEngineCertCenter  = DeploymentProviderType(AccessProviderTypeVolcEngine + \"-certcenter\")\n\tDeploymentProviderTypeVolcEngineCLB         = DeploymentProviderType(AccessProviderTypeVolcEngine + \"-clb\")\n\tDeploymentProviderTypeVolcEngineDCDN        = DeploymentProviderType(AccessProviderTypeVolcEngine + \"-dcdn\")\n\tDeploymentProviderTypeVolcEngineImageX      = DeploymentProviderType(AccessProviderTypeVolcEngine + \"-imagex\")\n\tDeploymentProviderTypeVolcEngineLive        = DeploymentProviderType(AccessProviderTypeVolcEngine + \"-live\")\n\tDeploymentProviderTypeVolcEngineTOS         = DeploymentProviderType(AccessProviderTypeVolcEngine + \"-tos\")\n\tDeploymentProviderTypeVolcEngineVOD         = DeploymentProviderType(AccessProviderTypeVolcEngine + \"-vod\")\n\tDeploymentProviderTypeVolcEngineWAF         = DeploymentProviderType(AccessProviderTypeVolcEngine + \"-waf\")\n\tDeploymentProviderTypeWangsuCDN             = DeploymentProviderType(AccessProviderTypeWangsu + \"-cdn\")\n\tDeploymentProviderTypeWangsuCDNPro          = DeploymentProviderType(AccessProviderTypeWangsu + \"-cdnpro\")\n\tDeploymentProviderTypeWangsuCertificate     = DeploymentProviderType(AccessProviderTypeWangsu + \"-certificate\")\n\tDeploymentProviderTypeWebhook               = DeploymentProviderType(AccessProviderTypeWebhook)\n)\n\ntype NotificationProviderType string\n\n/*\n消息通知提供商常量值。\n短横线前的部分始终等于授权提供商类型。\n\n注意：如果追加新的常量值，请保持以 ASCII 排序。\nNOTICE: If you add new constant, please keep ASCII order.\n*/\nconst (\n\tNotificationProviderTypeDingTalkBot = NotificationProviderType(AccessProviderTypeDingTalkBot)\n\tNotificationProviderTypeDiscordBot  = NotificationProviderType(AccessProviderTypeDiscordBot)\n\tNotificationProviderTypeEmail       = NotificationProviderType(AccessProviderTypeEmail)\n\tNotificationProviderTypeLarkBot     = NotificationProviderType(AccessProviderTypeLarkBot)\n\tNotificationProviderTypeMattermost  = NotificationProviderType(AccessProviderTypeMattermost)\n\tNotificationProviderTypeSlackBot    = NotificationProviderType(AccessProviderTypeSlackBot)\n\tNotificationProviderTypeTelegramBot = NotificationProviderType(AccessProviderTypeTelegramBot)\n\tNotificationProviderTypeWebhook     = NotificationProviderType(AccessProviderTypeWebhook)\n\tNotificationProviderTypeWeComBot    = NotificationProviderType(AccessProviderTypeWeComBot)\n)\n"
  },
  {
    "path": "internal/domain/settings.go",
    "content": "package domain\n\nimport (\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nconst CollectionNameSettings = \"settings\"\n\ntype Settings struct {\n\tMeta\n\tName    string          `db:\"name\"    json:\"name\"`\n\tContent SettingsContent `db:\"content\" json:\"content\"`\n}\n\nconst (\n\tSettingsNameEmails               = \"emails\"\n\tSettingsNameNotificationTemplate = \"notifyTemplate\"\n\tSettingsNameScriptTemplate       = \"scriptTemplate\"\n\tSettingsNameSSLProvider          = \"sslProvider\"\n\tSettingsNamePersistence          = \"persistence\"\n)\n\ntype SettingsContent map[string]any\n\ntype SettingsContentForSSLProvider struct {\n\tProvider CAProviderType                    `json:\"provider\"`\n\tConfigs  map[CAProviderType]map[string]any `json:\"configs\"`\n\tTimeout  int                               `json:\"timeout\"`\n}\n\ntype SettingsContentForPersistence struct {\n\tCertificatesWarningDaysBeforeExpire int `json:\"certificatesWarningDaysBeforeExpire\"`\n\tCertificatesRetentionMaxDays        int `json:\"certificatesRetentionMaxDays\"`\n\tWorkflowRunsRetentionMaxDays        int `json:\"workflowRunsRetentionMaxDays\"`\n}\n\nfunc (c SettingsContent) AsSSLProvider() *SettingsContentForSSLProvider {\n\tcontent := &SettingsContentForSSLProvider{}\n\txmaps.Populate(c, content)\n\n\tif content.Provider == \"\" {\n\t\tcontent.Provider = CAProviderTypeLetsEncrypt\n\t}\n\n\tif content.Timeout < 0 {\n\t\tcontent.Timeout = 0\n\t}\n\n\treturn content\n}\n\nfunc (c SettingsContent) AsPersistence() *SettingsContentForPersistence {\n\tcontent := &SettingsContentForPersistence{}\n\txmaps.Populate(c, content)\n\n\tif content.CertificatesWarningDaysBeforeExpire <= 0 {\n\t\tcontent.CertificatesWarningDaysBeforeExpire = 21\n\t}\n\n\tif content.CertificatesRetentionMaxDays < 0 {\n\t\tcontent.CertificatesRetentionMaxDays = 0\n\t}\n\n\tif content.WorkflowRunsRetentionMaxDays < 0 {\n\t\tcontent.WorkflowRunsRetentionMaxDays = 0\n\t}\n\n\treturn content\n}\n"
  },
  {
    "path": "internal/domain/statistics.go",
    "content": "package domain\n\ntype Statistics struct {\n\tCertificateTotal        int `json:\"certificateTotal\"`\n\tCertificateExpiringSoon int `json:\"certificateExpiringSoon\"`\n\tCertificateExpired      int `json:\"certificateExpired\"`\n\n\tWorkflowTotal    int `json:\"workflowTotal\"`\n\tWorkflowEnabled  int `json:\"workflowEnabled\"`\n\tWorkflowDisabled int `json:\"workflowDisabled\"`\n}\n"
  },
  {
    "path": "internal/domain/workflow.go",
    "content": "package domain\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/internal/domain/expr\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nconst CollectionNameWorkflow = \"workflow\"\n\ntype Workflow struct {\n\tMeta\n\tName          string                `db:\"name\"          json:\"name\"`\n\tDescription   string                `db:\"description\"   json:\"description\"`\n\tTrigger       WorkflowTriggerType   `db:\"trigger\"       json:\"trigger\"`\n\tTriggerCron   string                `db:\"triggerCron\"   json:\"triggerCron\"`\n\tEnabled       bool                  `db:\"enabled\"       json:\"enabled\"`\n\tGraphDraft    *WorkflowGraph        `db:\"graphDraft\"    json:\"graphDraft\"`\n\tGraphContent  *WorkflowGraph        `db:\"graphContent\"  json:\"graphContent\"`\n\tHasDraft      bool                  `db:\"hasDraft\"      json:\"hasDraft\"`\n\tHasContent    bool                  `db:\"hasContent\"    json:\"hasContent\"`\n\tLastRunId     string                `db:\"lastRunRef\"    json:\"lastRunId\"`\n\tLastRunStatus WorkflowRunStatusType `db:\"lastRunStatus\" json:\"lastRunStatus\"`\n\tLastRunTime   time.Time             `db:\"lastRunTime\"   json:\"lastRunTime\"`\n}\n\ntype WorkflowGraph struct {\n\tNodes []*WorkflowNode `json:\"nodes\"`\n}\n\nfunc (g *WorkflowGraph) GetNodeById(nodeId string) (*WorkflowNode, bool) {\n\treturn g.getNodeInBlocksById(g.Nodes, nodeId)\n}\n\nfunc (g *WorkflowGraph) getNodeInBlocksById(blocks []*WorkflowNode, nodeId string) (*WorkflowNode, bool) {\n\tfor _, node := range blocks {\n\t\tif node.Id == nodeId {\n\t\t\treturn node, true\n\t\t}\n\n\t\tif len(node.Blocks) > 0 {\n\t\t\tif found, ok := g.getNodeInBlocksById(node.Blocks, nodeId); ok {\n\t\t\t\treturn found, true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, false\n}\n\nfunc (g *WorkflowGraph) Verify() error {\n\tif len(g.Nodes) < 2 {\n\t\treturn fmt.Errorf(\"invalid nodes length of graph\")\n\t} else if g.Nodes[0].Type != WorkflowNodeTypeStart {\n\t\treturn fmt.Errorf(\"the first node is not a start node\")\n\t} else if g.Nodes[len(g.Nodes)-1].Type != WorkflowNodeTypeEnd {\n\t\treturn fmt.Errorf(\"the last node is not an end node\")\n\t}\n\n\treturn nil\n}\n\nfunc (g *WorkflowGraph) Clone() *WorkflowGraph {\n\treturn &WorkflowGraph{\n\t\tNodes: g.Nodes,\n\t}\n}\n\ntype WorkflowTriggerType string\n\nconst (\n\tWorkflowTriggerTypeScheduled = WorkflowTriggerType(\"scheduled\")\n\tWorkflowTriggerTypeManual    = WorkflowTriggerType(\"manual\")\n)\n\ntype WorkflowNode struct {\n\tId     string           `json:\"id\"` // 节点 ID 只在该工作流中唯一，在全局中不保证唯一性\n\tType   WorkflowNodeType `json:\"type\"`\n\tData   WorkflowNodeData `json:\"data\"`\n\tBlocks []*WorkflowNode  `json:\"blocks,omitempty\"`\n}\n\ntype WorkflowNodeType string\n\nconst (\n\tWorkflowNodeTypeStart       = WorkflowNodeType(\"start\")\n\tWorkflowNodeTypeEnd         = WorkflowNodeType(\"end\")\n\tWorkflowNodeTypeCondition   = WorkflowNodeType(\"condition\")\n\tWorkflowNodeTypeBranchBlock = WorkflowNodeType(\"branchBlock\")\n\tWorkflowNodeTypeTryCatch    = WorkflowNodeType(\"tryCatch\")\n\tWorkflowNodeTypeTryBlock    = WorkflowNodeType(\"tryBlock\")\n\tWorkflowNodeTypeCatchBlock  = WorkflowNodeType(\"catchBlock\")\n\tWorkflowNodeTypeDelay       = WorkflowNodeType(\"delay\")\n\tWorkflowNodeTypeBizApply    = WorkflowNodeType(\"bizApply\")\n\tWorkflowNodeTypeBizUpload   = WorkflowNodeType(\"bizUpload\")\n\tWorkflowNodeTypeBizMonitor  = WorkflowNodeType(\"bizMonitor\")\n\tWorkflowNodeTypeBizDeploy   = WorkflowNodeType(\"bizDeploy\")\n\tWorkflowNodeTypeBizNotify   = WorkflowNodeType(\"bizNotify\")\n)\n\ntype WorkflowNodeData struct {\n\tName     string             `json:\"name\"`\n\tDisabled bool               `json:\"disabled,omitempty,omitzero\"`\n\tConfig   WorkflowNodeConfig `json:\"config,omitempty,omitzero\"`\n}\n\ntype WorkflowNodeConfig map[string]any\n\nfunc (c WorkflowNodeConfig) AsDelay() WorkflowNodeConfigForDelay {\n\treturn WorkflowNodeConfigForDelay{\n\t\tWait: xmaps.GetInt(c, \"wait\"),\n\t}\n}\n\nfunc (c WorkflowNodeConfig) AsBranchBlock() WorkflowNodeConfigForBranchBlock {\n\texpression := c[\"expression\"]\n\tif expression == nil {\n\t\treturn WorkflowNodeConfigForBranchBlock{}\n\t}\n\n\texprRaw, _ := json.Marshal(expression)\n\texpr, err := expr.UnmarshalExpr([]byte(exprRaw))\n\tif err != nil {\n\t\treturn WorkflowNodeConfigForBranchBlock{}\n\t}\n\n\treturn WorkflowNodeConfigForBranchBlock{\n\t\tExpression: expr,\n\t}\n}\n\nfunc (c WorkflowNodeConfig) AsBizApply() WorkflowNodeConfigForBizApply {\n\tdomains := lo.Filter(strings.Split(xmaps.GetString(c, \"domains\"), \";\"), func(s string, _ int) bool { return s != \"\" })\n\tipaddrs := lo.Filter(strings.Split(xmaps.GetString(c, \"ipaddrs\"), \";\"), func(s string, _ int) bool { return s != \"\" })\n\tnameservers := lo.Filter(strings.Split(xmaps.GetString(c, \"nameservers\"), \";\"), func(s string, _ int) bool { return s != \"\" })\n\n\treturn WorkflowNodeConfigForBizApply{\n\t\tDomains:               domains,\n\t\tIPAddrs:               ipaddrs,\n\t\tContactEmail:          xmaps.GetString(c, \"contactEmail\"),\n\t\tChallengeType:         xmaps.GetString(c, \"challengeType\"),\n\t\tProvider:              xmaps.GetString(c, \"provider\"),\n\t\tProviderAccessId:      xmaps.GetString(c, \"providerAccessId\"),\n\t\tProviderConfig:        xmaps.GetKVMapAny(c, \"providerConfig\"),\n\t\tKeySource:             xmaps.GetOrDefaultString(c, \"keySource\", \"auto\"),\n\t\tKeyAlgorithm:          xmaps.GetOrDefaultString(c, \"keyAlgorithm\", string(CertificateKeyAlgorithmTypeRSA2048)),\n\t\tKeyContent:            xmaps.GetString(c, \"keyContent\"),\n\t\tCAProvider:            xmaps.GetString(c, \"caProvider\"),\n\t\tCAProviderAccessId:    xmaps.GetString(c, \"caProviderAccessId\"),\n\t\tCAProviderConfig:      xmaps.GetKVMapAny(c, \"caProviderConfig\"),\n\t\tValidityLifetime:      xmaps.GetString(c, \"validityLifetime\"),\n\t\tPreferredChain:        xmaps.GetString(c, \"preferredChain\"),\n\t\tACMEProfile:           xmaps.GetString(c, \"acmeProfile\"),\n\t\tNameservers:           nameservers,\n\t\tDnsPropagationWait:    xmaps.GetInt(c, \"dnsPropagationWait\"),\n\t\tDnsPropagationTimeout: xmaps.GetInt(c, \"dnsPropagationTimeout\"),\n\t\tDnsTTL:                xmaps.GetInt(c, \"dnsTTL\"),\n\t\tHttpDelayWait:         xmaps.GetInt(c, \"httpDelayWait\"),\n\t\tDisableCommonName:     xmaps.GetBool(c, \"disableCommonName\"),\n\t\tDisableFollowCNAME:    xmaps.GetBool(c, \"disableFollowCNAME\"),\n\t\tDisableARI:            xmaps.GetBool(c, \"disableARI\"),\n\t\tSkipBeforeExpiryDays:  xmaps.GetInt(c, \"skipBeforeExpiryDays\"),\n\t}\n}\n\nfunc (c WorkflowNodeConfig) AsBizUpload() WorkflowNodeConfigForBizUpload {\n\treturn WorkflowNodeConfigForBizUpload{\n\t\tSource:      xmaps.GetOrDefaultString(c, \"source\", \"form\"),\n\t\tCertificate: xmaps.GetString(c, \"certificate\"),\n\t\tPrivateKey:  xmaps.GetString(c, \"privateKey\"),\n\t}\n}\n\nfunc (c WorkflowNodeConfig) AsBizMonitor() WorkflowNodeConfigForBizMonitor {\n\thost := xmaps.GetString(c, \"host\")\n\treturn WorkflowNodeConfigForBizMonitor{\n\t\tHost:        host,\n\t\tPort:        xmaps.GetOrDefaultInt32(c, \"port\", 443),\n\t\tDomain:      xmaps.GetOrDefaultString(c, \"domain\", host),\n\t\tRequestPath: xmaps.GetString(c, \"path\"),\n\t}\n}\n\nfunc (c WorkflowNodeConfig) AsBizDeploy() WorkflowNodeConfigForBizDeploy {\n\treturn WorkflowNodeConfigForBizDeploy{\n\t\tCertificateOutputNodeId: xmaps.GetString(c, \"certificateOutputNodeId\"),\n\t\tProvider:                xmaps.GetString(c, \"provider\"),\n\t\tProviderAccessId:        xmaps.GetString(c, \"providerAccessId\"),\n\t\tProviderConfig:          xmaps.GetKVMapAny(c, \"providerConfig\"),\n\t\tSkipOnLastSucceeded:     xmaps.GetBool(c, \"skipOnLastSucceeded\"),\n\t}\n}\n\nfunc (c WorkflowNodeConfig) AsBizNotify() WorkflowNodeConfigForBizNotify {\n\treturn WorkflowNodeConfigForBizNotify{\n\t\tProvider:             xmaps.GetString(c, \"provider\"),\n\t\tProviderAccessId:     xmaps.GetString(c, \"providerAccessId\"),\n\t\tProviderConfig:       xmaps.GetKVMapAny(c, \"providerConfig\"),\n\t\tSubject:              xmaps.GetString(c, \"subject\"),\n\t\tMessage:              xmaps.GetString(c, \"message\"),\n\t\tSkipOnAllPrevSkipped: xmaps.GetBool(c, \"skipOnAllPrevSkipped\"),\n\t}\n}\n\ntype WorkflowNodeConfigForDelay struct {\n\tWait int `json:\"wait\"` // 等待时间\n}\n\ntype WorkflowNodeConfigForBranchBlock struct {\n\tExpression expr.Expr `json:\"expression\"` // 条件表达式\n}\n\ntype WorkflowNodeConfigForBizApply struct {\n\tDomains               []string       `json:\"domains\"`                         // 域名列表，以半角分号分隔\n\tIPAddrs               []string       `json:\"ipaddrs\"`                         // IP 地址列表，以半角分号分隔\n\tContactEmail          string         `json:\"contactEmail\"`                    // 联系邮箱\n\tChallengeType         string         `json:\"challengeType\"`                   // 质询方式\n\tProvider              string         `json:\"provider\"`                        // 质询提供商\n\tProviderAccessId      string         `json:\"providerAccessId\"`                // 质询提供商授权记录 ID\n\tProviderConfig        map[string]any `json:\"providerConfig,omitempty\"`        // 质询提供商额外配置\n\tCAProvider            string         `json:\"caProvider,omitempty\"`            // CA 提供商（零值时使用全局配置）\n\tCAProviderAccessId    string         `json:\"caProviderAccessId,omitempty\"`    // CA 提供商授权记录 ID\n\tCAProviderConfig      map[string]any `json:\"caProviderConfig,omitempty\"`      // CA 提供商额外配置\n\tKeySource             string         `json:\"keySource\"`                       // 私钥来源，可取值 \"auto\"、\"reuse\"、\"custom\"（零值时默认值 \"auto\"）\n\tKeyAlgorithm          string         `json:\"keyAlgorithm,omitempty\"`          // 私钥算法\n\tKeyContent            string         `json:\"keyContent,omitempty\"`            // 私钥内容\n\tValidityLifetime      string         `json:\"validityLifetime,omitempty\"`      // 有效期，形如 \"30d\"、\"6h\"\n\tPreferredChain        string         `json:\"preferredChain,omitempty\"`        // 首选证书链\n\tACMEProfile           string         `json:\"acmeProfile,omitempty\"`           // ACME Profiles Extension\n\tNameservers           []string       `json:\"nameservers,omitempty\"`           // DNS 服务器列表，以半角分号分隔。等同于 lego 的 `--dns.resolvers` 参数\n\tDnsPropagationWait    int            `json:\"dnsPropagationWait,omitempty\"`    // DNS 传播等待时间。等同于 lego 的 `--dns.propagation-wait` 参数\n\tDnsPropagationTimeout int            `json:\"dnsPropagationTimeout,omitempty\"` // DNS 传播检查超时时间。等同于 lego 的 `--dns-timeout` 参数\n\tDnsTTL                int            `json:\"dnsTTL,omitempty\"`                // DNS 解析记录 TTL\n\tHttpDelayWait         int            `json:\"httpDelayWait,omitempty\"`         // HTTP 等待时间。等同于 lego 的 `--http.delay` 参数\n\tDisableCommonName     bool           `json:\"disableCommonName,omitempty\"`     // 是否不包含 CommonName。等同于 lego 的 `--disable-cn` 参数\n\tDisableFollowCNAME    bool           `json:\"disableFollowCNAME,omitempty\"`    // 是否关闭 CNAME 跟随\n\tDisableARI            bool           `json:\"disableARI,omitempty\"`            // 是否关闭 ARI\n\tSkipBeforeExpiryDays  int            `json:\"skipBeforeExpiryDays,omitempty\"`  // 证书到期前多少天前跳过续期\n}\n\ntype WorkflowNodeConfigForBizUpload struct {\n\tSource      string `json:\"source\"`      // 证书来源，可取值 \"form\"、\"local\"、\"url\"（零值时默认值 \"form\"）\n\tCertificate string `json:\"certificate\"` // 证书，根据证书来源决定是 PEM 内容 / 文件路径 / URL\n\tPrivateKey  string `json:\"privateKey\"`  // 私钥，根据证书来源决定是 PEM 内容 / 文件路径 / URL\n}\n\ntype WorkflowNodeConfigForBizMonitor struct {\n\tHost        string `json:\"host\"`                  // 主机地址\n\tPort        int32  `json:\"port,omitempty\"`        // 端口（零值时默认值 443）\n\tDomain      string `json:\"domain,omitempty\"`      // 域名（零值时默认值 [Host]）\n\tRequestPath string `json:\"requestPath,omitempty\"` // 请求路径\n}\n\ntype WorkflowNodeConfigForBizDeploy struct {\n\tCertificateOutputNodeId string         `json:\"certificateOutputNodeId\"`    // 前序证书输出节点 ID\n\tProvider                string         `json:\"provider\"`                   // 主机提供商\n\tProviderAccessId        string         `json:\"providerAccessId,omitempty\"` // 主机提供商授权记录 ID\n\tProviderConfig          map[string]any `json:\"providerConfig,omitempty\"`   // 主机提供商额外配置\n\tSkipOnLastSucceeded     bool           `json:\"skipOnLastSucceeded\"`        // 上次部署成功时是否跳过\n}\n\ntype WorkflowNodeConfigForBizNotify struct {\n\tProvider             string         `json:\"provider\"`                 // 通知提供商\n\tProviderAccessId     string         `json:\"providerAccessId\"`         // 通知提供商授权记录 ID\n\tProviderConfig       map[string]any `json:\"providerConfig,omitempty\"` // 通知提供商额外配置\n\tSubject              string         `json:\"subject\"`                  // 通知主题\n\tMessage              string         `json:\"message\"`                  // 通知内容\n\tSkipOnAllPrevSkipped bool           `json:\"skipOnAllPrevSkipped\"`     // 前序节点均已跳过时是否跳过\n}\n"
  },
  {
    "path": "internal/domain/workflow_log.go",
    "content": "package domain\n\nimport (\n\t\"log/slog\"\n\t\"strings\"\n)\n\nconst CollectionNameWorkflowLog = \"workflow_logs\"\n\ntype WorkflowLog struct {\n\tMeta\n\tWorkflowId     string         `db:\"workflowRef\" json:\"workflowId\"`\n\tRunId          string         `db:\"runRef\"      json:\"runId\"`\n\tNodeId         string         `db:\"nodeId\"      json:\"nodeId\"`\n\tNodeName       string         `db:\"nodeName\"    json:\"nodeName\"`\n\tTimestampMilli int64          `db:\"timestamp\"   json:\"timestamp\"`\n\tLevel          int32          `db:\"level\"       json:\"level\"`\n\tMessage        string         `db:\"message\"     json:\"message\"`\n\tData           map[string]any `db:\"data\"        json:\"data\"`\n}\n\ntype WorkflowLogs []WorkflowLog\n\nfunc (r WorkflowLogs) ErrorString() string {\n\tvar builder strings.Builder\n\tfor _, log := range r {\n\t\tif log.Level >= int32(slog.LevelError) {\n\t\t\tbuilder.WriteString(log.Message)\n\t\t\tbuilder.WriteString(\"\\n\")\n\t\t}\n\t}\n\treturn strings.TrimSpace(builder.String())\n}\n"
  },
  {
    "path": "internal/domain/workflow_output.go",
    "content": "package domain\n\nconst CollectionNameWorkflowOutput = \"workflow_output\"\n\ntype WorkflowOutput struct {\n\tMeta\n\tWorkflowId string                 `db:\"workflowRef\" json:\"workflowId\"`\n\tRunId      string                 `db:\"runRef\"      json:\"runId\"`\n\tNodeId     string                 `db:\"nodeId\"      json:\"nodeId\"`\n\tNodeConfig WorkflowNodeConfig     `db:\"nodeConfig\"  json:\"nodeConfig\"`\n\tOutputs    []*WorkflowOutputEntry `db:\"outputs\"     json:\"outputs\"`\n\tSucceeded  bool                   `db:\"succeeded\"   json:\"succeeded\"`\n}\n\ntype WorkflowOutputEntry struct {\n\tType      string `json:\"type\"`\n\tName      string `json:\"name\"`\n\tValue     string `json:\"value\"`\n\tValueType string `json:\"valueType\"`\n}\n"
  },
  {
    "path": "internal/domain/workflow_run.go",
    "content": "package domain\n\nimport (\n\t\"time\"\n)\n\nconst CollectionNameWorkflowRun = \"workflow_run\"\n\ntype WorkflowRun struct {\n\tMeta\n\tWorkflowId string                `db:\"workflowRef\" json:\"workflowId\"`\n\tStatus     WorkflowRunStatusType `db:\"status\"      json:\"status\"`\n\tTrigger    WorkflowTriggerType   `db:\"trigger\"     json:\"trigger\"`\n\tStartedAt  time.Time             `db:\"startedAt\"   json:\"startedAt\"`\n\tEndedAt    time.Time             `db:\"endedAt\"     json:\"endedAt\"`\n\tGraph      *WorkflowGraph        `db:\"graph\"       json:\"graph\"`\n\tError      string                `db:\"error\"       json:\"error\"`\n}\n\ntype WorkflowRunStatusType string\n\nconst (\n\tWorkflowRunStatusTypePending    WorkflowRunStatusType = \"pending\"\n\tWorkflowRunStatusTypeProcessing WorkflowRunStatusType = \"processing\"\n\tWorkflowRunStatusTypeSucceeded  WorkflowRunStatusType = \"succeeded\"\n\tWorkflowRunStatusTypeFailed     WorkflowRunStatusType = \"failed\"\n\tWorkflowRunStatusTypeCanceled   WorkflowRunStatusType = \"canceled\"\n)\n"
  },
  {
    "path": "internal/notify/client.go",
    "content": "package notify\n\nimport (\n\t\"log/slog\"\n)\n\ntype Client struct {\n\tlogger *slog.Logger\n}\n\ntype ClientConfigure func(*Client)\n\nfunc NewClient(configures ...ClientConfigure) *Client {\n\tclient := &Client{}\n\tfor _, configure := range configures {\n\t\tconfigure(client)\n\t}\n\treturn client\n}\n\nfunc WithLogger(logger *slog.Logger) ClientConfigure {\n\treturn func(c *Client) {\n\t\tc.logger = logger\n\t}\n}\n"
  },
  {
    "path": "internal/notify/client_notifier.go",
    "content": "package notify\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/internal/notify/notifiers\"\n)\n\ntype SendNotificationRequest struct {\n\t// 提供商相关\n\tProvider               string\n\tProviderAccessConfig   map[string]any\n\tProviderExtendedConfig map[string]any\n\n\t// 通知相关\n\tSubject string\n\tMessage string\n}\n\ntype SendNotificationResponse struct{}\n\nfunc (c *Client) SendNotification(ctx context.Context, request *SendNotificationRequest) (*SendNotificationResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"the request is nil\")\n\t}\n\n\tproviderFactory, err := notifiers.Registries.Get(domain.NotificationProviderType(request.Provider))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprovider, err := providerFactory(&notifiers.ProviderFactoryOptions{\n\t\tProviderAccessConfig:   request.ProviderAccessConfig,\n\t\tProviderExtendedConfig: request.ProviderExtendedConfig,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to initialize notification provider '%s': %w\", request.Provider, err)\n\t}\n\n\tprovider.SetLogger(c.logger)\n\tif _, err := provider.Notify(ctx, request.Subject, request.Message); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &SendNotificationResponse{}, nil\n}\n"
  },
  {
    "path": "internal/notify/notifiers/registry.go",
    "content": "package notifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n)\n\ntype ProviderFactoryFunc func(options *ProviderFactoryOptions) (notifier.Provider, error)\n\ntype ProviderFactoryOptions struct {\n\tProviderAccessConfig   map[string]any\n\tProviderExtendedConfig map[string]any\n}\n\ntype Registry[T comparable] interface {\n\tRegister(T, ProviderFactoryFunc) error\n\tMustRegister(T, ProviderFactoryFunc)\n\tGet(T) (ProviderFactoryFunc, error)\n}\n\ntype registry[T comparable] struct {\n\tfactories map[T]ProviderFactoryFunc\n}\n\nfunc (r *registry[T]) Register(name T, factory ProviderFactoryFunc) error {\n\tif _, exists := r.factories[name]; exists {\n\t\treturn fmt.Errorf(\"provider '%v' already registered\", name)\n\t}\n\n\tr.factories[name] = factory\n\treturn nil\n}\n\nfunc (r *registry[T]) MustRegister(name T, factory ProviderFactoryFunc) {\n\tif err := r.Register(name, factory); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc (r *registry[T]) Get(name T) (ProviderFactoryFunc, error) {\n\tif factory, exists := r.factories[name]; exists {\n\t\treturn factory, nil\n\t}\n\n\treturn nil, fmt.Errorf(\"provider '%v' not registered\", name)\n}\n\nfunc newRegistry[T comparable]() Registry[T] {\n\treturn &registry[T]{factories: make(map[T]ProviderFactoryFunc)}\n}\n\nvar Registries = newRegistry[domain.NotificationProviderType]()\n"
  },
  {
    "path": "internal/notify/notifiers/sp_dingtalkbot.go",
    "content": "package notifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier/providers/dingtalkbot\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.NotificationProviderTypeDingTalkBot, func(options *ProviderFactoryOptions) (notifier.Provider, error) {\n\t\tcredentials := domain.AccessConfigForDingTalkBot{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := dingtalkbot.NewNotifier(&dingtalkbot.NotifierConfig{\n\t\t\tWebhookUrl:    credentials.WebhookUrl,\n\t\t\tSecret:        credentials.Secret,\n\t\t\tCustomPayload: credentials.CustomPayload,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/notify/notifiers/sp_discordbot.go",
    "content": "package notifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier/providers/discordbot\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.NotificationProviderTypeDiscordBot, func(options *ProviderFactoryOptions) (notifier.Provider, error) {\n\t\tcredentials := domain.AccessConfigForDiscordBot{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := discordbot.NewNotifier(&discordbot.NotifierConfig{\n\t\t\tBotToken:  credentials.BotToken,\n\t\t\tChannelId: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"channelId\", credentials.ChannelId),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/notify/notifiers/sp_email.go",
    "content": "package notifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier/providers/email\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.NotificationProviderTypeEmail, func(options *ProviderFactoryOptions) (notifier.Provider, error) {\n\t\tcredentials := domain.AccessConfigForEmail{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := email.NewNotifier(&email.NotifierConfig{\n\t\t\tSmtpHost:                 credentials.SmtpHost,\n\t\t\tSmtpPort:                 credentials.SmtpPort,\n\t\t\tSmtpTls:                  credentials.SmtpTls,\n\t\t\tUsername:                 credentials.Username,\n\t\t\tPassword:                 credentials.Password,\n\t\t\tSenderAddress:            credentials.SenderAddress,\n\t\t\tSenderName:               credentials.SenderName,\n\t\t\tReceiverAddress:          xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"receiverAddress\", credentials.ReceiverAddress),\n\t\t\tMessageFormat:            xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"format\", email.MESSAGE_FORMAT_PLAIN),\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/notify/notifiers/sp_larkbot.go",
    "content": "package notifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier/providers/larkbot\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.NotificationProviderTypeLarkBot, func(options *ProviderFactoryOptions) (notifier.Provider, error) {\n\t\tcredentials := domain.AccessConfigForLarkBot{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := larkbot.NewNotifier(&larkbot.NotifierConfig{\n\t\t\tWebhookUrl:    credentials.WebhookUrl,\n\t\t\tSecret:        credentials.Secret,\n\t\t\tCustomPayload: credentials.CustomPayload,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/notify/notifiers/sp_mattermost.go",
    "content": "package notifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier/providers/mattermost\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.NotificationProviderTypeMattermost, func(options *ProviderFactoryOptions) (notifier.Provider, error) {\n\t\tcredentials := domain.AccessConfigForMattermost{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := mattermost.NewNotifier(&mattermost.NotifierConfig{\n\t\t\tServerUrl: credentials.ServerUrl,\n\t\t\tUsername:  credentials.Username,\n\t\t\tPassword:  credentials.Password,\n\t\t\tChannelId: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"channelId\", credentials.ChannelId),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/notify/notifiers/sp_slackbot.go",
    "content": "package notifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n\tslackbot \"github.com/certimate-go/certimate/pkg/core/notifier/providers/slackbot\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.NotificationProviderTypeSlackBot, func(options *ProviderFactoryOptions) (notifier.Provider, error) {\n\t\tcredentials := domain.AccessConfigForSlackBot{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := slackbot.NewNotifier(&slackbot.NotifierConfig{\n\t\t\tBotToken:  credentials.BotToken,\n\t\t\tChannelId: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"channelId\", credentials.ChannelId),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/notify/notifiers/sp_telegrambot.go",
    "content": "package notifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier/providers/telegrambot\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.NotificationProviderTypeTelegramBot, func(options *ProviderFactoryOptions) (notifier.Provider, error) {\n\t\tcredentials := domain.AccessConfigForTelegramBot{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := telegrambot.NewNotifier(&telegrambot.NotifierConfig{\n\t\t\tBotToken: credentials.BotToken,\n\t\t\tChatId:   xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"chatId\", credentials.ChatId),\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/notify/notifiers/sp_webhook.go",
    "content": "package notifiers\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier/providers/webhook\"\n\txhttp \"github.com/certimate-go/certimate/pkg/utils/http\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.NotificationProviderTypeWebhook, func(options *ProviderFactoryOptions) (notifier.Provider, error) {\n\t\tcredentials := domain.AccessConfigForWebhook{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tmergedHeaders := make(map[string]string)\n\t\tif defaultHeadersString := credentials.HeadersString; defaultHeadersString != \"\" {\n\t\t\th, err := xhttp.ParseHeaders(defaultHeadersString)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to parse webhook headers: %w\", err)\n\t\t\t}\n\t\t\tfor key := range h {\n\t\t\t\tmergedHeaders[http.CanonicalHeaderKey(key)] = h.Get(key)\n\t\t\t}\n\t\t}\n\t\tif extendedHeadersString := xmaps.GetString(options.ProviderExtendedConfig, \"headers\"); extendedHeadersString != \"\" {\n\t\t\th, err := xhttp.ParseHeaders(extendedHeadersString)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to parse webhook headers: %w\", err)\n\t\t\t}\n\t\t\tfor key := range h {\n\t\t\t\tmergedHeaders[http.CanonicalHeaderKey(key)] = h.Get(key)\n\t\t\t}\n\t\t}\n\n\t\tprovider, err := webhook.NewNotifier(&webhook.NotifierConfig{\n\t\t\tWebhookUrl:               credentials.Url,\n\t\t\tWebhookData:              xmaps.GetOrDefaultString(options.ProviderExtendedConfig, \"webhookData\", credentials.DataString),\n\t\t\tMethod:                   credentials.Method,\n\t\t\tHeaders:                  mergedHeaders,\n\t\t\tTimeout:                  xmaps.GetInt(options.ProviderExtendedConfig, \"timeout\"),\n\t\t\tAllowInsecureConnections: credentials.AllowInsecureConnections,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/notify/notifiers/sp_wecombot.go",
    "content": "package notifiers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier/providers/wecombot\"\n\txmaps \"github.com/certimate-go/certimate/pkg/utils/maps\"\n)\n\nfunc init() {\n\tRegistries.MustRegister(domain.NotificationProviderTypeWeComBot, func(options *ProviderFactoryOptions) (notifier.Provider, error) {\n\t\tcredentials := domain.AccessConfigForWeComBot{}\n\t\tif err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to populate provider access config: %w\", err)\n\t\t}\n\n\t\tprovider, err := wecombot.NewNotifier(&wecombot.NotifierConfig{\n\t\t\tWebhookUrl:    credentials.WebhookUrl,\n\t\t\tCustomPayload: credentials.CustomPayload,\n\t\t})\n\t\treturn provider, err\n\t})\n}\n"
  },
  {
    "path": "internal/notify/service.go",
    "content": "package notify\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/certimate-go/certimate/internal/domain/dtos\"\n)\n\nconst (\n\ttestSubject = \"[Certimate] Notification Testing\"\n\ttestMessage = \"Welcome to use Certimate!\"\n)\n\ntype NotifyService struct {\n\taccessRepo accessRepository\n}\n\nfunc NewNotifyService(accessRepo accessRepository) *NotifyService {\n\treturn &NotifyService{\n\t\taccessRepo: accessRepo,\n\t}\n}\n\nfunc (n *NotifyService) TestPush(ctx context.Context, req *dtos.NotifyTestPushReq) (*dtos.NotifyTestPushResp, error) {\n\taccessConfig := make(map[string]any)\n\tif access, err := n.accessRepo.GetById(ctx, req.AccessId); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get access #%s record: %w\", req.AccessId, err)\n\t} else {\n\t\tif access.Reserve != \"notif\" {\n\t\t\treturn nil, fmt.Errorf(\"access #%s is not available for notification\", req.AccessId)\n\t\t}\n\n\t\taccessConfig = access.Config\n\t}\n\n\tnotifier := NewClient()\n\tnotifyReq := &SendNotificationRequest{\n\t\tProvider:               req.Provider,\n\t\tProviderAccessConfig:   accessConfig,\n\t\tProviderExtendedConfig: make(map[string]any),\n\t\tSubject:                testSubject,\n\t\tMessage:                testMessage,\n\t}\n\tif _, err := notifier.SendNotification(ctx, notifyReq); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &dtos.NotifyTestPushResp{}, nil\n}\n"
  },
  {
    "path": "internal/notify/service_deps.go",
    "content": "package notify\n\nimport (\n\t\"context\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\ntype accessRepository interface {\n\tGetById(ctx context.Context, id string) (*domain.Access, error)\n}\n"
  },
  {
    "path": "internal/repository/access.go",
    "content": "package repository\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\ntype AccessRepository struct{}\n\nfunc NewAccessRepository() *AccessRepository {\n\treturn &AccessRepository{}\n}\n\nfunc (r *AccessRepository) GetById(ctx context.Context, id string) (*domain.Access, error) {\n\trecord, err := app.GetApp().FindRecordById(domain.CollectionNameAccess, id)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, domain.ErrRecordNotFound\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tif !record.GetDateTime(\"deleted\").Time().IsZero() {\n\t\treturn nil, domain.ErrRecordNotFound\n\t}\n\n\treturn r.castRecordToModel(record)\n}\n\nfunc (r *AccessRepository) castRecordToModel(record *core.Record) (*domain.Access, error) {\n\tif record == nil {\n\t\treturn nil, errors.New(\"the record is nil\")\n\t}\n\n\tconfig := make(map[string]any)\n\tif err := record.UnmarshalJSONField(\"config\", &config); err != nil {\n\t\treturn nil, errors.New(\"field 'config' is malformed\")\n\t}\n\n\taccess := &domain.Access{\n\t\tMeta: domain.Meta{\n\t\t\tId:        record.Id,\n\t\t\tCreatedAt: record.GetDateTime(\"created\").Time(),\n\t\t\tUpdatedAt: record.GetDateTime(\"updated\").Time(),\n\t\t},\n\t\tName:     record.GetString(\"name\"),\n\t\tProvider: record.GetString(\"provider\"),\n\t\tConfig:   config,\n\t\tReserve:  record.GetString(\"reserve\"),\n\t}\n\treturn access, nil\n}\n"
  },
  {
    "path": "internal/repository/acme_account.go",
    "content": "package repository\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\ntype ACMEAccountRepository struct{}\n\nfunc NewACMEAccountRepository() *ACMEAccountRepository {\n\treturn &ACMEAccountRepository{}\n}\n\nfunc (r *ACMEAccountRepository) GetByCAAndEmail(ctx context.Context, ca, caDirUrl, email string) (*domain.ACMEAccount, error) {\n\trecord, err := app.GetApp().FindFirstRecordByFilter(\n\t\tdomain.CollectionNameACMEAccount,\n\t\t\"ca={:ca} && acmeDirUrl={:acmeDirUrl} && email={:email}\",\n\t\tdbx.Params{\"ca\": ca, \"acmeDirUrl\": caDirUrl, \"email\": email},\n\t)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, domain.ErrRecordNotFound\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn r.castRecordToModel(record)\n}\n\nfunc (r *ACMEAccountRepository) GetByAcctUrl(ctx context.Context, acctUrl string) (*domain.ACMEAccount, error) {\n\trecord, err := app.GetApp().FindFirstRecordByFilter(\n\t\tdomain.CollectionNameACMEAccount,\n\t\t\"acmeAcctUrl={:acmeAcctUrl}\",\n\t\tdbx.Params{\"acmeAcctUrl\": acctUrl},\n\t)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, domain.ErrRecordNotFound\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn r.castRecordToModel(record)\n}\n\nfunc (r *ACMEAccountRepository) Save(ctx context.Context, acmeAccount *domain.ACMEAccount) (*domain.ACMEAccount, error) {\n\tcollection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameACMEAccount)\n\tif err != nil {\n\t\treturn acmeAccount, err\n\t}\n\n\tvar record *core.Record\n\tif acmeAccount.Id == \"\" {\n\t\trecord = core.NewRecord(collection)\n\t} else {\n\t\trecord, err = app.GetApp().FindRecordById(collection, acmeAccount.Id)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\t\treturn acmeAccount, domain.ErrRecordNotFound\n\t\t\t}\n\t\t\treturn acmeAccount, err\n\t\t}\n\t}\n\n\trecord.Set(\"ca\", acmeAccount.CA)\n\trecord.Set(\"email\", acmeAccount.Email)\n\trecord.Set(\"privateKey\", acmeAccount.PrivateKey)\n\trecord.Set(\"acmeAccount\", acmeAccount.ACMEAccount)\n\trecord.Set(\"acmeAcctUrl\", acmeAccount.ACMEAcctUrl)\n\trecord.Set(\"acmeDirUrl\", acmeAccount.ACMEDirUrl)\n\tif err := app.GetApp().Save(record); err != nil {\n\t\treturn acmeAccount, err\n\t}\n\n\tacmeAccount.Id = record.Id\n\tacmeAccount.CreatedAt = record.GetDateTime(\"created\").Time()\n\tacmeAccount.UpdatedAt = record.GetDateTime(\"updated\").Time()\n\treturn acmeAccount, nil\n}\n\nfunc (r *ACMEAccountRepository) castRecordToModel(record *core.Record) (*domain.ACMEAccount, error) {\n\tif record == nil {\n\t\treturn nil, errors.New(\"the record is nil\")\n\t}\n\n\taccount := &acme.Account{}\n\tif err := record.UnmarshalJSONField(\"acmeAccount\", account); err != nil {\n\t\treturn nil, errors.New(\"field 'acmeAccount' is malformed\")\n\t}\n\n\tacmeAccount := &domain.ACMEAccount{\n\t\tMeta: domain.Meta{\n\t\t\tId:        record.Id,\n\t\t\tCreatedAt: record.GetDateTime(\"created\").Time(),\n\t\t\tUpdatedAt: record.GetDateTime(\"updated\").Time(),\n\t\t},\n\t\tCA:          record.GetString(\"ca\"),\n\t\tEmail:       record.GetString(\"email\"),\n\t\tPrivateKey:  record.GetString(\"privateKey\"),\n\t\tACMEAccount: account,\n\t\tACMEAcctUrl: record.GetString(\"acmeAcctUrl\"),\n\t\tACMEDirUrl:  record.GetString(\"acmeDirUrl\"),\n\t}\n\treturn acmeAccount, nil\n}\n"
  },
  {
    "path": "internal/repository/certificate.go",
    "content": "package repository\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\ntype CertificateRepository struct{}\n\nfunc NewCertificateRepository() *CertificateRepository {\n\treturn &CertificateRepository{}\n}\n\nfunc (r *CertificateRepository) ListExpiringSoon(ctx context.Context) ([]*domain.Certificate, error) {\n\trecords, err := app.GetApp().FindAllRecords(\n\t\tdomain.CollectionNameCertificate,\n\t\tdbx.NewExp(\"validityNotAfter>DATETIME('now')\"),\n\t\tdbx.NewExp(\"validityNotAfter<DATETIME('now', '+20 days')\"),\n\t\tdbx.NewExp(\"deleted=null\"),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcertificates := make([]*domain.Certificate, 0)\n\tfor _, record := range records {\n\t\tcertificate, err := r.castRecordToModel(record)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tcertificates = append(certificates, certificate)\n\t}\n\n\treturn certificates, nil\n}\n\nfunc (r *CertificateRepository) GetById(ctx context.Context, id string) (*domain.Certificate, error) {\n\trecord, err := app.GetApp().FindRecordById(domain.CollectionNameCertificate, id)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, domain.ErrRecordNotFound\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tif !record.GetDateTime(\"deleted\").Time().IsZero() {\n\t\treturn nil, domain.ErrRecordNotFound\n\t}\n\n\treturn r.castRecordToModel(record)\n}\n\nfunc (r *CertificateRepository) GetByWorkflowIdAndNodeId(ctx context.Context, workflowId string, workflowNodeId string) (*domain.Certificate, error) {\n\trecords, err := app.GetApp().FindRecordsByFilter(\n\t\tdomain.CollectionNameCertificate,\n\t\t\"workflowRef={:workflowId} && workflowNodeId={:workflowNodeId} && deleted=null\",\n\t\t\"-created\",\n\t\t1, 0,\n\t\tdbx.Params{\"workflowId\": workflowId},\n\t\tdbx.Params{\"workflowNodeId\": workflowNodeId},\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(records) == 0 {\n\t\treturn nil, domain.ErrRecordNotFound\n\t}\n\n\treturn r.castRecordToModel(records[0])\n}\n\nfunc (r *CertificateRepository) GetByWorkflowRunIdAndNodeId(ctx context.Context, workflowRunId string, workflowNodeId string) (*domain.Certificate, error) {\n\trecords, err := app.GetApp().FindRecordsByFilter(\n\t\tdomain.CollectionNameCertificate,\n\t\t\"workflowRunRef={:workflowRunId} && workflowNodeId={:workflowNodeId} && deleted=null\",\n\t\t\"-created\",\n\t\t1, 0,\n\t\tdbx.Params{\"workflowRunId\": workflowRunId},\n\t\tdbx.Params{\"workflowNodeId\": workflowNodeId},\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(records) == 0 {\n\t\treturn nil, domain.ErrRecordNotFound\n\t}\n\n\treturn r.castRecordToModel(records[0])\n}\n\nfunc (r *CertificateRepository) Save(ctx context.Context, certificate *domain.Certificate) (*domain.Certificate, error) {\n\tcollection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameCertificate)\n\tif err != nil {\n\t\treturn certificate, err\n\t}\n\n\tvar record *core.Record\n\tif certificate.Id == \"\" {\n\t\trecord = core.NewRecord(collection)\n\t} else {\n\t\trecord, err = app.GetApp().FindRecordById(collection, certificate.Id)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\t\treturn certificate, domain.ErrRecordNotFound\n\t\t\t}\n\t\t\treturn certificate, err\n\t\t}\n\t}\n\n\trecord.Set(\"source\", string(certificate.Source))\n\trecord.Set(\"subjectAltNames\", certificate.SubjectAltNames)\n\trecord.Set(\"serialNumber\", certificate.SerialNumber)\n\trecord.Set(\"certificate\", certificate.Certificate)\n\trecord.Set(\"privateKey\", certificate.PrivateKey)\n\trecord.Set(\"issuerOrg\", certificate.IssuerOrg)\n\trecord.Set(\"issuerCertificate\", certificate.IssuerCertificate)\n\trecord.Set(\"keyAlgorithm\", string(certificate.KeyAlgorithm))\n\trecord.Set(\"validityNotBefore\", certificate.ValidityNotBefore)\n\trecord.Set(\"validityNotAfter\", certificate.ValidityNotAfter)\n\trecord.Set(\"validityInterval\", certificate.ValidityInterval)\n\trecord.Set(\"acmeAcctUrl\", certificate.ACMEAcctUrl)\n\trecord.Set(\"acmeCertUrl\", certificate.ACMECertUrl)\n\trecord.Set(\"isRenewed\", certificate.IsRenewed)\n\trecord.Set(\"isRevoked\", certificate.IsRevoked)\n\trecord.Set(\"workflowRef\", certificate.WorkflowId)\n\trecord.Set(\"workflowRunRef\", certificate.WorkflowRunId)\n\trecord.Set(\"workflowNodeId\", certificate.WorkflowNodeId)\n\tif err := app.GetApp().Save(record); err != nil {\n\t\treturn certificate, err\n\t}\n\n\tcertificate.Id = record.Id\n\tcertificate.CreatedAt = record.GetDateTime(\"created\").Time()\n\tcertificate.UpdatedAt = record.GetDateTime(\"updated\").Time()\n\treturn certificate, nil\n}\n\nfunc (r *CertificateRepository) DeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error) {\n\trecords, err := app.GetApp().FindAllRecords(domain.CollectionNameCertificate, exprs...)\n\tif err != nil {\n\t\treturn 0, nil\n\t}\n\n\tvar ret int\n\tvar errs []error\n\tfor _, record := range records {\n\t\tif err := app.GetApp().Delete(record); err != nil {\n\t\t\terrs = append(errs, err)\n\t\t} else {\n\t\t\tret++\n\t\t}\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn ret, errors.Join(errs...)\n\t}\n\n\treturn ret, nil\n}\n\nfunc (r *CertificateRepository) castRecordToModel(record *core.Record) (*domain.Certificate, error) {\n\tif record == nil {\n\t\treturn nil, errors.New(\"the record is nil\")\n\t}\n\n\tcertificate := &domain.Certificate{\n\t\tMeta: domain.Meta{\n\t\t\tId:        record.Id,\n\t\t\tCreatedAt: record.GetDateTime(\"created\").Time(),\n\t\t\tUpdatedAt: record.GetDateTime(\"updated\").Time(),\n\t\t},\n\t\tSource:            domain.CertificateSourceType(record.GetString(\"source\")),\n\t\tSubjectAltNames:   record.GetString(\"subjectAltNames\"),\n\t\tSerialNumber:      record.GetString(\"serialNumber\"),\n\t\tCertificate:       record.GetString(\"certificate\"),\n\t\tPrivateKey:        record.GetString(\"privateKey\"),\n\t\tIssuerOrg:         record.GetString(\"issuerOrg\"),\n\t\tIssuerCertificate: record.GetString(\"issuerCertificate\"),\n\t\tKeyAlgorithm:      domain.CertificateKeyAlgorithmType(record.GetString(\"keyAlgorithm\")),\n\t\tValidityNotBefore: record.GetDateTime(\"validityNotBefore\").Time(),\n\t\tValidityNotAfter:  record.GetDateTime(\"validityNotAfter\").Time(),\n\t\tValidityInterval:  int32(record.GetInt(\"validityInterval\")),\n\t\tACMEAcctUrl:       record.GetString(\"acmeAcctUrl\"),\n\t\tACMECertUrl:       record.GetString(\"acmeCertUrl\"),\n\t\tIsRenewed:         record.GetBool(\"isRenewed\"),\n\t\tIsRevoked:         record.GetBool(\"isRevoked\"),\n\t\tWorkflowId:        record.GetString(\"workflowRef\"),\n\t\tWorkflowRunId:     record.GetString(\"workflowRunRef\"),\n\t\tWorkflowNodeId:    record.GetString(\"workflowNodeId\"),\n\t}\n\treturn certificate, nil\n}\n"
  },
  {
    "path": "internal/repository/settings.go",
    "content": "package repository\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/pocketbase/dbx\"\n)\n\ntype SettingsRepository struct{}\n\nfunc NewSettingsRepository() *SettingsRepository {\n\treturn &SettingsRepository{}\n}\n\nfunc (r *SettingsRepository) GetByName(ctx context.Context, name string) (*domain.Settings, error) {\n\trecord, err := app.GetApp().FindFirstRecordByFilter(\n\t\tdomain.CollectionNameSettings,\n\t\t\"name={:name}\",\n\t\tdbx.Params{\"name\": name},\n\t)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, domain.ErrRecordNotFound\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tcontent := make(map[string]any)\n\tif err := record.UnmarshalJSONField(\"content\", &content); err != nil {\n\t\treturn nil, errors.New(\"field 'content' is malformed\")\n\t}\n\n\tsettings := &domain.Settings{\n\t\tMeta: domain.Meta{\n\t\t\tId:        record.Id,\n\t\t\tCreatedAt: record.GetDateTime(\"created\").Time(),\n\t\t\tUpdatedAt: record.GetDateTime(\"updated\").Time(),\n\t\t},\n\t\tName:    record.GetString(\"name\"),\n\t\tContent: content,\n\t}\n\treturn settings, nil\n}\n"
  },
  {
    "path": "internal/repository/statistics.go",
    "content": "package repository\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/pocketbase/dbx\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\ntype StatisticsRepository struct{}\n\nfunc NewStatisticsRepository() *StatisticsRepository {\n\treturn &StatisticsRepository{}\n}\n\nfunc (r *StatisticsRepository) Get(ctx context.Context) (*domain.Statistics, error) {\n\tstatistics := &domain.Statistics{}\n\n\t// 读取设置\n\tvar persistenceSettings *domain.SettingsContentForPersistence\n\trsSettings := struct {\n\t\tContent string `db:\"content\"`\n\t}{}\n\tif err := app.GetDB().\n\t\tNewQuery(fmt.Sprintf(\"SELECT content FROM %s WHERE name = {:name}\", domain.CollectionNameSettings)).\n\t\tBind(dbx.Params{\"name\": domain.SettingsNamePersistence}).\n\t\tOne(&rsSettings); err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\tpersistenceSettings = (domain.SettingsContent{}).AsPersistence()\n\t\t} else {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tjson.Unmarshal([]byte(rsSettings.Content), &persistenceSettings)\n\t}\n\n\t// 统计所有证书\n\trsCertTotal := struct {\n\t\tTotal int `db:\"total\"`\n\t}{}\n\tif err := app.GetDB().\n\t\tNewQuery(fmt.Sprintf(\"SELECT COUNT(*) AS total FROM %s WHERE deleted = ''\", domain.CollectionNameCertificate)).\n\t\tOne(&rsCertTotal); err != nil {\n\t\treturn nil, err\n\t}\n\tstatistics.CertificateTotal = rsCertTotal.Total\n\n\t// 统计即将过期证书\n\trsCertExpiringSoonTotal := struct {\n\t\tTotal int `db:\"total\"`\n\t}{}\n\tif err := app.GetDB().\n\t\tNewQuery(fmt.Sprintf(\"SELECT COUNT(*) AS total FROM %s WHERE validityNotAfter <= DATETIME('now', '+%d days') AND validityNotAfter > DATETIME('now') AND isRevoked = 0 AND deleted = ''\", domain.CollectionNameCertificate, persistenceSettings.CertificatesWarningDaysBeforeExpire)).\n\t\tOne(&rsCertExpiringSoonTotal); err != nil {\n\t\treturn nil, err\n\t}\n\tstatistics.CertificateExpiringSoon = rsCertExpiringSoonTotal.Total\n\n\t// 统计已过期证书\n\trsCertExpiredTotal := struct {\n\t\tTotal int `db:\"total\"`\n\t}{}\n\tif err := app.GetDB().\n\t\tNewQuery(fmt.Sprintf(\"SELECT COUNT(*) AS total FROM %s WHERE validityNotAfter <= DATETIME('now') AND deleted = ''\", domain.CollectionNameCertificate)).\n\t\tOne(&rsCertExpiredTotal); err != nil {\n\t\treturn nil, err\n\t}\n\tstatistics.CertificateExpired = rsCertExpiredTotal.Total\n\n\t// 统计所有工作流\n\trsWorkflowTotal := struct {\n\t\tTotal int `db:\"total\"`\n\t}{}\n\tif err := app.GetDB().\n\t\tNewQuery(fmt.Sprintf(\"SELECT COUNT(*) AS total FROM %s\", domain.CollectionNameWorkflow)).\n\t\tOne(&rsWorkflowTotal); err != nil {\n\t\treturn nil, err\n\t}\n\tstatistics.WorkflowTotal = rsWorkflowTotal.Total\n\n\t// 统计已启用工作流\n\trsWorkflowEnabledTotal := struct {\n\t\tTotal int `db:\"total\"`\n\t}{}\n\tif err := app.GetDB().\n\t\tNewQuery(fmt.Sprintf(\"SELECT COUNT(*) AS total FROM %s WHERE enabled IS TRUE\", domain.CollectionNameWorkflow)).\n\t\tOne(&rsWorkflowEnabledTotal); err != nil {\n\t\treturn nil, err\n\t}\n\tstatistics.WorkflowEnabled = rsWorkflowEnabledTotal.Total\n\tstatistics.WorkflowDisabled = rsWorkflowTotal.Total - rsWorkflowEnabledTotal.Total\n\n\treturn statistics, nil\n}\n"
  },
  {
    "path": "internal/repository/workflow.go",
    "content": "package repository\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\ntype WorkflowRepository struct{}\n\nfunc NewWorkflowRepository() *WorkflowRepository {\n\treturn &WorkflowRepository{}\n}\n\nfunc (r *WorkflowRepository) ListEnabledScheduled(ctx context.Context) ([]*domain.Workflow, error) {\n\trecords, err := app.GetApp().FindRecordsByFilter(\n\t\tdomain.CollectionNameWorkflow,\n\t\t\"enabled={:enabled} && trigger={:trigger}\",\n\t\t\"-created\",\n\t\t0, 0,\n\t\tdbx.Params{\"enabled\": true, \"trigger\": string(domain.WorkflowTriggerTypeScheduled)},\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tworkflows := make([]*domain.Workflow, 0)\n\tfor _, record := range records {\n\t\tworkflow, err := r.castRecordToModel(record)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tworkflows = append(workflows, workflow)\n\t}\n\n\treturn workflows, nil\n}\n\nfunc (r *WorkflowRepository) GetById(ctx context.Context, id string) (*domain.Workflow, error) {\n\trecord, err := app.GetApp().FindRecordById(domain.CollectionNameWorkflow, id)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, domain.ErrRecordNotFound\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn r.castRecordToModel(record)\n}\n\nfunc (r *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow) (*domain.Workflow, error) {\n\tcollection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflow)\n\tif err != nil {\n\t\treturn workflow, err\n\t}\n\n\tvar record *core.Record\n\tif workflow.Id == \"\" {\n\t\trecord = core.NewRecord(collection)\n\t} else {\n\t\trecord, err = app.GetApp().FindRecordById(collection, workflow.Id)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\t\treturn workflow, domain.ErrRecordNotFound\n\t\t\t}\n\t\t\treturn workflow, err\n\t\t}\n\t}\n\n\trecord.Set(\"name\", workflow.Name)\n\trecord.Set(\"description\", workflow.Description)\n\trecord.Set(\"trigger\", string(workflow.Trigger))\n\trecord.Set(\"triggerCron\", workflow.TriggerCron)\n\trecord.Set(\"enabled\", workflow.Enabled)\n\trecord.Set(\"graphDraft\", workflow.GraphDraft)\n\trecord.Set(\"graphContent\", workflow.GraphContent)\n\trecord.Set(\"hasDraft\", workflow.HasDraft)\n\trecord.Set(\"hasContent\", workflow.HasContent)\n\trecord.Set(\"lastRunRef\", workflow.LastRunId)\n\trecord.Set(\"lastRunStatus\", string(workflow.LastRunStatus))\n\trecord.Set(\"lastRunTime\", workflow.LastRunTime)\n\tif err := app.GetApp().Save(record); err != nil {\n\t\treturn workflow, err\n\t}\n\n\tworkflow.Id = record.Id\n\tworkflow.CreatedAt = record.GetDateTime(\"created\").Time()\n\tworkflow.UpdatedAt = record.GetDateTime(\"updated\").Time()\n\treturn workflow, nil\n}\n\nfunc (r *WorkflowRepository) castRecordToModel(record *core.Record) (*domain.Workflow, error) {\n\tif record == nil {\n\t\treturn nil, errors.New(\"the record is nil\")\n\t}\n\n\tgraphDraft := &domain.WorkflowGraph{}\n\tif err := record.UnmarshalJSONField(\"graphDraft\", graphDraft); err != nil {\n\t\treturn nil, errors.New(\"field 'graphDraft' is malformed\")\n\t}\n\n\tgraphContent := &domain.WorkflowGraph{}\n\tif err := record.UnmarshalJSONField(\"graphContent\", graphContent); err != nil {\n\t\treturn nil, errors.New(\"field 'graphContent' is malformed\")\n\t}\n\n\tworkflow := &domain.Workflow{\n\t\tMeta: domain.Meta{\n\t\t\tId:        record.Id,\n\t\t\tCreatedAt: record.GetDateTime(\"created\").Time(),\n\t\t\tUpdatedAt: record.GetDateTime(\"updated\").Time(),\n\t\t},\n\t\tName:          record.GetString(\"name\"),\n\t\tDescription:   record.GetString(\"description\"),\n\t\tTrigger:       domain.WorkflowTriggerType(record.GetString(\"trigger\")),\n\t\tTriggerCron:   record.GetString(\"triggerCron\"),\n\t\tEnabled:       record.GetBool(\"enabled\"),\n\t\tGraphDraft:    graphDraft,\n\t\tGraphContent:  graphContent,\n\t\tHasDraft:      record.GetBool(\"hasDraft\"),\n\t\tHasContent:    record.GetBool(\"hasContent\"),\n\t\tLastRunId:     record.GetString(\"lastRunRef\"),\n\t\tLastRunStatus: domain.WorkflowRunStatusType(record.GetString(\"lastRunStatus\")),\n\t\tLastRunTime:   record.GetDateTime(\"lastRunTime\").Time(),\n\t}\n\treturn workflow, nil\n}\n"
  },
  {
    "path": "internal/repository/workflow_log.go",
    "content": "package repository\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\ntype WorkflowLogRepository struct{}\n\nfunc NewWorkflowLogRepository() *WorkflowLogRepository {\n\treturn &WorkflowLogRepository{}\n}\n\nfunc (r *WorkflowLogRepository) ListByWorkflowRunId(ctx context.Context, workflowRunId string) ([]*domain.WorkflowLog, error) {\n\trecords, err := app.GetApp().FindRecordsByFilter(\n\t\tdomain.CollectionNameWorkflowLog,\n\t\t\"runRef={:runId}\",\n\t\t\"timestamp\",\n\t\t0, 0,\n\t\tdbx.Params{\"runId\": workflowRunId},\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tworkflowLogs := make([]*domain.WorkflowLog, 0)\n\tfor _, record := range records {\n\t\tworkflowLog, err := r.castRecordToModel(record)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tworkflowLogs = append(workflowLogs, workflowLog)\n\t}\n\n\treturn workflowLogs, nil\n}\n\nfunc (r *WorkflowLogRepository) Save(ctx context.Context, workflowLog *domain.WorkflowLog) (*domain.WorkflowLog, error) {\n\tcollection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowLog)\n\tif err != nil {\n\t\treturn workflowLog, err\n\t}\n\n\tvar record *core.Record\n\tif workflowLog.Id == \"\" {\n\t\trecord = core.NewRecord(collection)\n\t} else {\n\t\trecord, err = app.GetApp().FindRecordById(collection, workflowLog.Id)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\t\treturn workflowLog, err\n\t\t\t}\n\t\t\trecord = core.NewRecord(collection)\n\t\t}\n\t}\n\n\trecord.Set(\"workflowRef\", workflowLog.WorkflowId)\n\trecord.Set(\"runRef\", workflowLog.RunId)\n\trecord.Set(\"nodeId\", workflowLog.NodeId)\n\trecord.Set(\"nodeName\", workflowLog.NodeName)\n\trecord.Set(\"timestamp\", workflowLog.TimestampMilli)\n\trecord.Set(\"level\", workflowLog.Level)\n\trecord.Set(\"message\", workflowLog.Message)\n\trecord.Set(\"data\", workflowLog.Data)\n\trecord.Set(\"created\", workflowLog.CreatedAt)\n\terr = app.GetApp().Save(record)\n\tif err != nil {\n\t\treturn workflowLog, err\n\t}\n\n\tworkflowLog.Id = record.Id\n\tworkflowLog.CreatedAt = record.GetDateTime(\"created\").Time()\n\tworkflowLog.UpdatedAt = record.GetDateTime(\"updated\").Time()\n\n\treturn workflowLog, nil\n}\n\nfunc (r *WorkflowLogRepository) castRecordToModel(record *core.Record) (*domain.WorkflowLog, error) {\n\tif record == nil {\n\t\treturn nil, errors.New(\"the record is nil\")\n\t}\n\n\tlogdata := make(map[string]any)\n\tif err := record.UnmarshalJSONField(\"data\", &logdata); err != nil {\n\t\treturn nil, errors.New(\"field 'data' is malformed\")\n\t}\n\n\tworkflowLog := &domain.WorkflowLog{\n\t\tMeta: domain.Meta{\n\t\t\tId:        record.Id,\n\t\t\tCreatedAt: record.GetDateTime(\"created\").Time(),\n\t\t\tUpdatedAt: record.GetDateTime(\"updated\").Time(),\n\t\t},\n\t\tWorkflowId:     record.GetString(\"workflowRef\"),\n\t\tRunId:          record.GetString(\"runRef\"),\n\t\tNodeId:         record.GetString(\"nodeId\"),\n\t\tNodeName:       record.GetString(\"nodeName\"),\n\t\tTimestampMilli: int64(record.GetInt(\"timestamp\")),\n\t\tLevel:          int32(record.GetInt(\"level\")),\n\t\tMessage:        record.GetString(\"message\"),\n\t\tData:           logdata,\n\t}\n\treturn workflowLog, nil\n}\n"
  },
  {
    "path": "internal/repository/workflow_output.go",
    "content": "package repository\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\ntype WorkflowOutputRepository struct{}\n\nfunc NewWorkflowOutputRepository() *WorkflowOutputRepository {\n\treturn &WorkflowOutputRepository{}\n}\n\nfunc (r *WorkflowOutputRepository) GetByWorkflowIdAndNodeId(ctx context.Context, workflowId string, workflowNodeId string) (*domain.WorkflowOutput, error) {\n\trecords, err := app.GetApp().FindRecordsByFilter(\n\t\tdomain.CollectionNameWorkflowOutput,\n\t\t\"workflowRef={:workflowId} && nodeId={:nodeId}\",\n\t\t\"-created\",\n\t\t1, 0,\n\t\tdbx.Params{\"workflowId\": workflowId},\n\t\tdbx.Params{\"nodeId\": workflowNodeId},\n\t)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, domain.ErrRecordNotFound\n\t\t}\n\t\treturn nil, err\n\t}\n\tif len(records) == 0 {\n\t\treturn nil, domain.ErrRecordNotFound\n\t}\n\n\treturn r.castRecordToModel(records[0])\n}\n\nfunc (r *WorkflowOutputRepository) GetByWorkflowRunIdAndNodeId(ctx context.Context, workflowRunId string, workflowNodeId string) (*domain.WorkflowOutput, error) {\n\trecords, err := app.GetApp().FindRecordsByFilter(\n\t\tdomain.CollectionNameWorkflowOutput,\n\t\t\"runRef={:workflowRunId} && nodeId={:nodeId}\",\n\t\t\"-created\",\n\t\t1, 0,\n\t\tdbx.Params{\"workflowRunId\": workflowRunId},\n\t\tdbx.Params{\"nodeId\": workflowNodeId},\n\t)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, domain.ErrRecordNotFound\n\t\t}\n\t\treturn nil, err\n\t}\n\tif len(records) == 0 {\n\t\treturn nil, domain.ErrRecordNotFound\n\t}\n\n\treturn r.castRecordToModel(records[0])\n}\n\nfunc (r *WorkflowOutputRepository) Save(ctx context.Context, workflowOutput *domain.WorkflowOutput) (*domain.WorkflowOutput, error) {\n\trecord, err := r.saveRecord(workflowOutput)\n\tif err != nil {\n\t\treturn workflowOutput, err\n\t}\n\n\tworkflowOutput.Id = record.Id\n\tworkflowOutput.CreatedAt = record.GetDateTime(\"created\").Time()\n\tworkflowOutput.UpdatedAt = record.GetDateTime(\"updated\").Time()\n\treturn workflowOutput, nil\n}\n\nfunc (r *WorkflowOutputRepository) castRecordToModel(record *core.Record) (*domain.WorkflowOutput, error) {\n\tif record == nil {\n\t\treturn nil, errors.New(\"the record is nil\")\n\t}\n\n\tnodeConfig := make(domain.WorkflowNodeConfig)\n\tif err := record.UnmarshalJSONField(\"nodeConfig\", &nodeConfig); err != nil {\n\t\treturn nil, errors.New(\"field 'nodeConfig' is malformed\")\n\t}\n\n\toutputs := make([]*domain.WorkflowOutputEntry, 0)\n\tif err := record.UnmarshalJSONField(\"outputs\", &outputs); err != nil {\n\t\treturn nil, errors.New(\"field 'outputs' is malformed\")\n\t}\n\n\tworkflowOutput := &domain.WorkflowOutput{\n\t\tMeta: domain.Meta{\n\t\t\tId:        record.Id,\n\t\t\tCreatedAt: record.GetDateTime(\"created\").Time(),\n\t\t\tUpdatedAt: record.GetDateTime(\"updated\").Time(),\n\t\t},\n\t\tWorkflowId: record.GetString(\"workflowRef\"),\n\t\tRunId:      record.GetString(\"runRef\"),\n\t\tNodeId:     record.GetString(\"nodeId\"),\n\t\tNodeConfig: nodeConfig,\n\t\tOutputs:    outputs,\n\t\tSucceeded:  record.GetBool(\"succeeded\"),\n\t}\n\treturn workflowOutput, nil\n}\n\nfunc (r *WorkflowOutputRepository) saveRecord(workflowOutput *domain.WorkflowOutput) (*core.Record, error) {\n\tcollection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowOutput)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar record *core.Record\n\tif workflowOutput.Id == \"\" {\n\t\trecord = core.NewRecord(collection)\n\t} else {\n\t\trecord, err = app.GetApp().FindRecordById(collection, workflowOutput.Id)\n\t\tif err != nil {\n\t\t\treturn record, err\n\t\t}\n\t}\n\trecord.Set(\"workflowRef\", workflowOutput.WorkflowId)\n\trecord.Set(\"runRef\", workflowOutput.RunId)\n\trecord.Set(\"nodeId\", workflowOutput.NodeId)\n\trecord.Set(\"nodeConfig\", workflowOutput.NodeConfig)\n\trecord.Set(\"outputs\", workflowOutput.Outputs)\n\trecord.Set(\"succeeded\", workflowOutput.Succeeded)\n\tif err := app.GetApp().Save(record); err != nil {\n\t\treturn record, err\n\t}\n\n\treturn record, nil\n}\n"
  },
  {
    "path": "internal/repository/workflow_run.go",
    "content": "package repository\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\ntype WorkflowRunRepository struct{}\n\nfunc NewWorkflowRunRepository() *WorkflowRunRepository {\n\treturn &WorkflowRunRepository{}\n}\n\nfunc (r *WorkflowRunRepository) GetById(ctx context.Context, id string) (*domain.WorkflowRun, error) {\n\trecord, err := app.GetApp().FindRecordById(domain.CollectionNameWorkflowRun, id)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, domain.ErrRecordNotFound\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn r.castRecordToModel(record)\n}\n\nfunc (r *WorkflowRunRepository) Save(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) {\n\tcollection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowRun)\n\tif err != nil {\n\t\treturn workflowRun, err\n\t}\n\n\tvar record *core.Record\n\tif workflowRun.Id == \"\" {\n\t\trecord = core.NewRecord(collection)\n\t} else {\n\t\trecord, err = app.GetApp().FindRecordById(collection, workflowRun.Id)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\t\treturn workflowRun, err\n\t\t\t}\n\t\t\trecord = core.NewRecord(collection)\n\t\t}\n\t}\n\n\trecord.Set(\"workflowRef\", workflowRun.WorkflowId)\n\trecord.Set(\"trigger\", string(workflowRun.Trigger))\n\trecord.Set(\"status\", string(workflowRun.Status))\n\trecord.Set(\"startedAt\", workflowRun.StartedAt)\n\trecord.Set(\"endedAt\", workflowRun.EndedAt)\n\trecord.Set(\"graph\", workflowRun.Graph)\n\trecord.Set(\"error\", workflowRun.Error)\n\terr = app.GetApp().Save(record)\n\tif err != nil {\n\t\treturn workflowRun, err\n\t}\n\n\tworkflowRun.Id = record.Id\n\tworkflowRun.CreatedAt = record.GetDateTime(\"created\").Time()\n\tworkflowRun.UpdatedAt = record.GetDateTime(\"updated\").Time()\n\treturn workflowRun, nil\n}\n\nfunc (r *WorkflowRunRepository) SaveWithCascading(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) {\n\tcollection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowRun)\n\tif err != nil {\n\t\treturn workflowRun, err\n\t}\n\n\tvar record *core.Record\n\tif workflowRun.Id == \"\" {\n\t\trecord = core.NewRecord(collection)\n\t} else {\n\t\trecord, err = app.GetApp().FindRecordById(collection, workflowRun.Id)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\t\treturn workflowRun, err\n\t\t\t}\n\t\t\trecord = core.NewRecord(collection)\n\t\t}\n\t}\n\n\terr = app.GetApp().RunInTransaction(func(txApp core.App) error {\n\t\trecord.Set(\"workflowRef\", workflowRun.WorkflowId)\n\t\trecord.Set(\"trigger\", string(workflowRun.Trigger))\n\t\trecord.Set(\"status\", string(workflowRun.Status))\n\t\trecord.Set(\"startedAt\", workflowRun.StartedAt)\n\t\trecord.Set(\"endedAt\", workflowRun.EndedAt)\n\t\trecord.Set(\"graph\", workflowRun.Graph)\n\t\trecord.Set(\"error\", workflowRun.Error)\n\t\terr = txApp.Save(record)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tworkflowRun.Id = record.Id\n\t\tworkflowRun.CreatedAt = record.GetDateTime(\"created\").Time()\n\t\tworkflowRun.UpdatedAt = record.GetDateTime(\"updated\").Time()\n\n\t\t// 事务级联更新所属工作流的最后运行记录\n\t\tworkflowRecord, err := txApp.FindRecordById(domain.CollectionNameWorkflow, workflowRun.WorkflowId)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t} else if workflowRun.Id == workflowRecord.GetString(\"lastRunRef\") {\n\t\t\tworkflowRecord.IgnoreUnchangedFields(true)\n\t\t\tworkflowRecord.Set(\"lastRunStatus\", record.GetString(\"status\"))\n\t\t\terr = txApp.Save(workflowRecord)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else if workflowRecord.GetDateTime(\"lastRunTime\").Time().IsZero() || workflowRun.StartedAt.After(workflowRecord.GetDateTime(\"lastRunTime\").Time()) {\n\t\t\tworkflowRecord.IgnoreUnchangedFields(true)\n\t\t\tworkflowRecord.Set(\"lastRunRef\", record.Id)\n\t\t\tworkflowRecord.Set(\"lastRunStatus\", record.GetString(\"status\"))\n\t\t\tworkflowRecord.Set(\"lastRunTime\", record.GetString(\"startedAt\"))\n\t\t\terr = txApp.Save(workflowRecord)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn workflowRun, err\n\t}\n\n\treturn workflowRun, nil\n}\n\nfunc (r *WorkflowRunRepository) DeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error) {\n\trecords, err := app.GetApp().FindAllRecords(domain.CollectionNameWorkflowRun, exprs...)\n\tif err != nil {\n\t\treturn 0, nil\n\t}\n\n\tvar ret int\n\tvar errs []error\n\tfor _, record := range records {\n\t\tif err := app.GetApp().Delete(record); err != nil {\n\t\t\terrs = append(errs, err)\n\t\t} else {\n\t\t\tret++\n\t\t}\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn ret, errors.Join(errs...)\n\t}\n\n\treturn ret, nil\n}\n\nfunc (r *WorkflowRunRepository) castRecordToModel(record *core.Record) (*domain.WorkflowRun, error) {\n\tif record == nil {\n\t\treturn nil, errors.New(\"the record is nil\")\n\t}\n\n\tgraph := &domain.WorkflowGraph{}\n\tif err := record.UnmarshalJSONField(\"graph\", &graph); err != nil {\n\t\treturn nil, errors.New(\"field 'graph' is malformed\")\n\t}\n\n\tworkflowRun := &domain.WorkflowRun{\n\t\tMeta: domain.Meta{\n\t\t\tId:        record.Id,\n\t\t\tCreatedAt: record.GetDateTime(\"created\").Time(),\n\t\t\tUpdatedAt: record.GetDateTime(\"updated\").Time(),\n\t\t},\n\t\tWorkflowId: record.GetString(\"workflowRef\"),\n\t\tStatus:     domain.WorkflowRunStatusType(record.GetString(\"status\")),\n\t\tTrigger:    domain.WorkflowTriggerType(record.GetString(\"trigger\")),\n\t\tStartedAt:  record.GetDateTime(\"startedAt\").Time(),\n\t\tEndedAt:    record.GetDateTime(\"endedAt\").Time(),\n\t\tGraph:      graph,\n\t\tError:      record.GetString(\"error\"),\n\t}\n\treturn workflowRun, nil\n}\n"
  },
  {
    "path": "internal/rest/handlers/certificates.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/pocketbase/tools/router\"\n\n\t\"github.com/certimate-go/certimate/internal/domain/dtos\"\n\t\"github.com/certimate-go/certimate/internal/rest/resp\"\n)\n\ntype certificateService interface {\n\tDownloadCertificate(ctx context.Context, req *dtos.CertificateDownloadReq) (*dtos.CertificateDownloadResp, error)\n\tRevokeCertificate(ctx context.Context, req *dtos.CertificateRevokeReq) (*dtos.CertificateRevokeResp, error)\n}\n\ntype CertificatesHandler struct {\n\tservice certificateService\n}\n\nfunc NewCertificatesHandler(router *router.RouterGroup[*core.RequestEvent], service certificateService) {\n\thandler := &CertificatesHandler{\n\t\tservice: service,\n\t}\n\n\tgroup := router.Group(\"/certificates\")\n\tgroup.POST(\"/{certificateId}/download\", handler.downloadCertificate)\n\tgroup.POST(\"/{certificateId}/revoke\", handler.revokeCertificate)\n\n\tgroup.POST(\"/{certificateId}/archive\", handler.downloadCertificate) // 兼容旧版\n}\n\nfunc (handler *CertificatesHandler) downloadCertificate(e *core.RequestEvent) error {\n\treq := &dtos.CertificateDownloadReq{}\n\treq.CertificateId = e.Request.PathValue(\"certificateId\")\n\tif err := e.BindBody(req); err != nil {\n\t\treturn resp.Err(e, err)\n\t}\n\n\tres, err := handler.service.DownloadCertificate(e.Request.Context(), req)\n\tif err != nil {\n\t\treturn resp.Err(e, err)\n\t}\n\n\treturn resp.Ok(e, res)\n}\n\nfunc (handler *CertificatesHandler) revokeCertificate(e *core.RequestEvent) error {\n\treq := &dtos.CertificateRevokeReq{}\n\treq.CertificateId = e.Request.PathValue(\"certificateId\")\n\tif err := e.BindBody(req); err != nil {\n\t\treturn resp.Err(e, err)\n\t}\n\n\tres, err := handler.service.RevokeCertificate(e.Request.Context(), req)\n\tif err != nil {\n\t\treturn resp.Err(e, err)\n\t}\n\n\treturn resp.Ok(e, res)\n}\n"
  },
  {
    "path": "internal/rest/handlers/notifications.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/pocketbase/tools/router\"\n\n\t\"github.com/certimate-go/certimate/internal/domain/dtos\"\n\t\"github.com/certimate-go/certimate/internal/rest/resp\"\n)\n\ntype notifyService interface {\n\tTestPush(ctx context.Context, req *dtos.NotifyTestPushReq) (*dtos.NotifyTestPushResp, error)\n}\n\ntype NotificationsHandler struct {\n\tservice notifyService\n}\n\nfunc NewNotificationsHandler(router *router.RouterGroup[*core.RequestEvent], service notifyService) {\n\thandler := &NotificationsHandler{\n\t\tservice: service,\n\t}\n\n\tgroup := router.Group(\"/notifications\")\n\tgroup.POST(\"/test\", handler.test)\n}\n\nfunc (handler *NotificationsHandler) test(e *core.RequestEvent) error {\n\treq := &dtos.NotifyTestPushReq{}\n\tif err := e.BindBody(req); err != nil {\n\t\treturn resp.Err(e, err)\n\t}\n\n\tres, err := handler.service.TestPush(e.Request.Context(), req)\n\tif err != nil {\n\t\treturn resp.Err(e, err)\n\t}\n\n\treturn resp.Ok(e, res)\n}\n"
  },
  {
    "path": "internal/rest/handlers/statistics.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/pocketbase/tools/router\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/internal/rest/resp\"\n)\n\ntype statisticsService interface {\n\tGet(ctx context.Context) (*domain.Statistics, error)\n}\n\ntype StatisticsHandler struct {\n\tservice statisticsService\n}\n\nfunc NewStatisticsHandler(router *router.RouterGroup[*core.RequestEvent], service statisticsService) {\n\thandler := &StatisticsHandler{\n\t\tservice: service,\n\t}\n\n\trouter.GET(\"/statistics\", handler.get)\n\n\trouter.GET(\"/statistics/get\", handler.get) // 兼容旧版\n}\n\nfunc (handler *StatisticsHandler) get(e *core.RequestEvent) error {\n\tres, err := handler.service.Get(e.Request.Context())\n\tif err != nil {\n\t\treturn resp.Err(e, err)\n\t}\n\n\treturn resp.Ok(e, res)\n}\n"
  },
  {
    "path": "internal/rest/handlers/workflows.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/pocketbase/tools/router\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/internal/domain/dtos\"\n\t\"github.com/certimate-go/certimate/internal/rest/resp\"\n)\n\ntype workflowService interface {\n\tGetStatistics(ctx context.Context) (*dtos.WorkflowStatisticsResp, error)\n\tStartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) (*dtos.WorkflowStartRunResp, error)\n\tCancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) (*dtos.WorkflowCancelRunResp, error)\n\tShutdown(ctx context.Context)\n}\n\ntype WorkflowsHandler struct {\n\tservice workflowService\n}\n\nfunc NewWorkflowsHandler(router *router.RouterGroup[*core.RequestEvent], service workflowService) {\n\thandler := &WorkflowsHandler{\n\t\tservice: service,\n\t}\n\n\tgroup := router.Group(\"/workflows\")\n\tgroup.GET(\"/stats\", handler.getStatistics)\n\tgroup.POST(\"/{workflowId}/runs\", handler.startRun)\n\tgroup.POST(\"/{workflowId}/runs/{runId}/cancel\", handler.cancelRun)\n}\n\nfunc (handler *WorkflowsHandler) getStatistics(e *core.RequestEvent) error {\n\tres, err := handler.service.GetStatistics(e.Request.Context())\n\tif err != nil {\n\t\treturn resp.Err(e, err)\n\t}\n\n\treturn resp.Ok(e, res)\n}\n\nfunc (handler *WorkflowsHandler) startRun(e *core.RequestEvent) error {\n\treq := &dtos.WorkflowStartRunReq{}\n\treq.WorkflowId = e.Request.PathValue(\"workflowId\")\n\tif err := e.BindBody(req); err != nil {\n\t\treturn resp.Err(e, err)\n\t}\n\tif req.RunTrigger != domain.WorkflowTriggerTypeManual {\n\t\treturn resp.Err(e, errors.New(\"invalid parameters: the value of 'trigger' must be 'manual'\"))\n\t}\n\n\tres, err := handler.service.StartRun(e.Request.Context(), req)\n\tif err != nil {\n\t\treturn resp.Err(e, err)\n\t}\n\n\treturn resp.Ok(e, res)\n}\n\nfunc (handler *WorkflowsHandler) cancelRun(e *core.RequestEvent) error {\n\treq := &dtos.WorkflowCancelRunReq{}\n\treq.WorkflowId = e.Request.PathValue(\"workflowId\")\n\treq.RunId = e.Request.PathValue(\"runId\")\n\n\tres, err := handler.service.CancelRun(e.Request.Context(), req)\n\tif err != nil {\n\t\treturn resp.Err(e, err)\n\t}\n\n\treturn resp.Ok(e, res)\n}\n"
  },
  {
    "path": "internal/rest/resp/resp.go",
    "content": "package resp\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\ntype Response struct {\n\tCode int         `json:\"code\"`\n\tMsg  string      `json:\"msg\"`\n\tData interface{} `json:\"data\"`\n}\n\nfunc Ok(e *core.RequestEvent, data interface{}) error {\n\trs := &Response{\n\t\tCode: 0,\n\t\tMsg:  \"success\",\n\t\tData: data,\n\t}\n\treturn e.JSON(http.StatusOK, rs)\n}\n\nfunc Err(e *core.RequestEvent, err error) error {\n\tcode := 500\n\n\txerr, ok := err.(*domain.Error)\n\tif ok {\n\t\tcode = xerr.Code\n\t}\n\n\trs := &Response{\n\t\tCode: code,\n\t\tMsg:  err.Error(),\n\t\tData: nil,\n\t}\n\treturn e.JSON(http.StatusOK, rs)\n}\n"
  },
  {
    "path": "internal/rest/routes/routes.go",
    "content": "package routes\n\nimport (\n\t\"github.com/pocketbase/pocketbase/apis\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/pocketbase/tools/router\"\n\n\t\"github.com/certimate-go/certimate/internal/certificate\"\n\t\"github.com/certimate-go/certimate/internal/notify\"\n\t\"github.com/certimate-go/certimate/internal/repository\"\n\t\"github.com/certimate-go/certimate/internal/rest/handlers\"\n\t\"github.com/certimate-go/certimate/internal/statistics\"\n\t\"github.com/certimate-go/certimate/internal/workflow\"\n)\n\nvar (\n\tcertificateSvc *certificate.CertificateService\n\tworkflowSvc    *workflow.WorkflowService\n\tstatisticsSvc  *statistics.StatisticsService\n\tnotifySvc      *notify.NotifyService\n)\n\nfunc BindRouter(router *router.Router[*core.RequestEvent]) {\n\taccessRepo := repository.NewAccessRepository()\n\tworkflowRepo := repository.NewWorkflowRepository()\n\tworkflowRunRepo := repository.NewWorkflowRunRepository()\n\tacmeAccountRepo := repository.NewACMEAccountRepository()\n\tcertificateRepo := repository.NewCertificateRepository()\n\tsettingsRepo := repository.NewSettingsRepository()\n\tstatisticsRepo := repository.NewStatisticsRepository()\n\n\tcertificateSvc = certificate.NewCertificateService(acmeAccountRepo, certificateRepo, settingsRepo)\n\tworkflowSvc = workflow.NewWorkflowService(workflowRepo, workflowRunRepo, settingsRepo)\n\tstatisticsSvc = statistics.NewStatisticsService(statisticsRepo)\n\tnotifySvc = notify.NewNotifyService(accessRepo)\n\n\tgroup := router.Group(\"/api\")\n\tgroup.Bind(apis.RequireSuperuserAuth())\n\thandlers.NewCertificatesHandler(group, certificateSvc)\n\thandlers.NewWorkflowsHandler(group, workflowSvc)\n\thandlers.NewStatisticsHandler(group, statisticsSvc)\n\thandlers.NewNotificationsHandler(group, notifySvc)\n}\n"
  },
  {
    "path": "internal/scheduler/certificate.go",
    "content": "package scheduler\n\nimport \"context\"\n\ntype certificateService interface {\n\tInitSchedule(ctx context.Context) error\n}\n\nfunc InitCertificateScheduler(service certificateService) error {\n\treturn service.InitSchedule(context.Background())\n}\n"
  },
  {
    "path": "internal/scheduler/scheduler.go",
    "content": "package scheduler\n\nimport (\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/certificate\"\n\t\"github.com/certimate-go/certimate/internal/repository\"\n\t\"github.com/certimate-go/certimate/internal/workflow\"\n)\n\nfunc Setup() {\n\tworkflowRepo := repository.NewWorkflowRepository()\n\tworkflowRunRepo := repository.NewWorkflowRunRepository()\n\tacmeAccountRepo := repository.NewACMEAccountRepository()\n\tcertificateRepo := repository.NewCertificateRepository()\n\tsettingsRepo := repository.NewSettingsRepository()\n\n\tworkflowSvc := workflow.NewWorkflowService(workflowRepo, workflowRunRepo, settingsRepo)\n\tcertificateSvc := certificate.NewCertificateService(acmeAccountRepo, certificateRepo, settingsRepo)\n\n\tif err := InitWorkflowScheduler(workflowSvc); err != nil {\n\t\tapp.GetLogger().Error(\"failed to init workflow scheduler\", slog.Any(\"error\", err))\n\t}\n\n\tif err := InitCertificateScheduler(certificateSvc); err != nil {\n\t\tapp.GetLogger().Error(\"failed to init certificate scheduler\", slog.Any(\"error\", err))\n\t}\n}\n"
  },
  {
    "path": "internal/scheduler/workflow.go",
    "content": "package scheduler\n\nimport \"context\"\n\ntype workflowService interface {\n\tInitSchedule(ctx context.Context) error\n}\n\nfunc InitWorkflowScheduler(service workflowService) error {\n\treturn service.InitSchedule(context.Background())\n}\n"
  },
  {
    "path": "internal/statistics/service.go",
    "content": "package statistics\n\nimport (\n\t\"context\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\ntype StatisticsService struct {\n\tstatRepo statisticsRepository\n}\n\nfunc NewStatisticsService(statRepo statisticsRepository) *StatisticsService {\n\treturn &StatisticsService{\n\t\tstatRepo: statRepo,\n\t}\n}\n\nfunc (s *StatisticsService) Get(ctx context.Context) (*domain.Statistics, error) {\n\treturn s.statRepo.Get(ctx)\n}\n"
  },
  {
    "path": "internal/statistics/service_deps.go",
    "content": "package statistics\n\nimport (\n\t\"context\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\ntype statisticsRepository interface {\n\tGet(ctx context.Context) (*domain.Statistics, error)\n}\n"
  },
  {
    "path": "internal/tools/mproc/receiver.go",
    "content": "package mproc\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\txcrypto \"github.com/certimate-go/certimate/pkg/utils/crypto\"\n)\n\ntype Receiver[TIn any, TOut any] interface {\n\tReceive(infile, outfile, enckey string) error\n\tReceiveWithContext(ctx context.Context, infile, outfile, enckey string) error\n}\n\ntype ReceiverHandler[TIn any, TOut any] func(ctx context.Context, params *TIn) (*TOut, error)\n\ntype receiver[TIn any, TOut any] struct {\n\thandler ReceiverHandler[TIn, TOut]\n}\n\nfunc (r *receiver[TIn, TOut]) Receive(infile, outfile, enckey string) error {\n\treturn r.ReceiveWithContext(context.Background(), infile, outfile, enckey)\n}\n\nfunc (r *receiver[TIn, TOut]) ReceiveWithContext(ctx context.Context, infile, outfile, enckey string) error {\n\tif infile == \"\" {\n\t\treturn errors.New(\"mproc: missing or invalid input file\")\n\t}\n\tif outfile == \"\" {\n\t\treturn errors.New(\"mproc: missing or invalid output file\")\n\t}\n\tif enckey == \"\" {\n\t\treturn errors.New(\"mproc: missing or invalid encryption key\")\n\t}\n\n\taesKey, err := hex.DecodeString(enckey)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mproc: missing or invalid encryption key: %w\", err)\n\t}\n\n\taesCryptor := xcrypto.NewAESCryptor(aesKey)\n\n\t// 读取输入\n\tinCipherData, err := os.ReadFile(infile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mproc: failed to read input file: %w\", err)\n\t}\n\n\t// 解密输入\n\tinPlainData, err := aesCryptor.CBCDecrypt(inCipherData)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mproc: failed to decrypt input data: %w\", err)\n\t}\n\n\t// 反序列化输入\n\tvar inData TIn\n\tif err := json.Unmarshal(inPlainData, &inData); err != nil {\n\t\treturn fmt.Errorf(\"mproc: failed to unmarshal input data: %w\", err)\n\t}\n\n\t// 处理\n\toutData, err := r.handler(ctx, &inData)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 序列化输出\n\toutPlainData, err := json.Marshal(outData)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mproc: failed to marshal output data: %w\", err)\n\t}\n\n\t// 加密输出\n\toutCipherData, err := aesCryptor.CBCEncrypt(outPlainData)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mproc: failed to encrypt output data: %w\", err)\n\t}\n\n\t// 写入输出\n\tif err := os.WriteFile(outfile, outCipherData, 0o644); err != nil {\n\t\treturn fmt.Errorf(\"mproc: failed to write output file: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// 创建并返回一个多进程指令接收器。\n//\n// 入参:\n//   - handler: 多进程指令处理函数。\n//\n// 出参:\n//   - 多进程指令接收器。\nfunc NewReceiver[TIn any, TOut any](handler ReceiverHandler[TIn, TOut]) Receiver[TIn, TOut] {\n\treturn &receiver[TIn, TOut]{handler: handler}\n}\n"
  },
  {
    "path": "internal/tools/mproc/sender.go",
    "content": "package mproc\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/go-cmd/cmd\"\n\n\txcrypto \"github.com/certimate-go/certimate/pkg/utils/crypto\"\n)\n\ntype Sender[TIn any, TOut any] interface {\n\tSend(params *TIn) (*TOut, error)\n\tSendWithContext(ctx context.Context, params *TIn) (*TOut, error)\n}\n\ntype sender[TIn any, TOut any] struct {\n\tcommand string\n\n\tlogger *slog.Logger\n}\n\nfunc (s *sender[TIn, TOut]) Send(params *TIn) (*TOut, error) {\n\treturn s.SendWithContext(context.Background(), params)\n}\n\nfunc (s *sender[TIn, TOut]) SendWithContext(ctx context.Context, params *TIn) (*TOut, error) {\n\t// 生成随机密钥\n\taesKey := make([]byte, 32)\n\tif _, err := rand.Read(aesKey); err != nil {\n\t\treturn nil, fmt.Errorf(\"mproc: failed to generate aes key: %w\", err)\n\t}\n\n\taesCryptor := xcrypto.NewAESCryptor(aesKey)\n\n\t// 准备临时输入文件\n\ttempIn, err := os.CreateTemp(\"\", \"certimate.mprocin_*.tmp\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mproc: failed to create temp input file: %w\", err)\n\t} else {\n\t\tinPlainData, err := json.Marshal(params)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mproc: failed to marshal input data: %w\", err)\n\t\t}\n\n\t\tinCipherData, err := aesCryptor.CBCEncrypt(inPlainData)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mproc: failed to encrypt input data: %w\", err)\n\t\t}\n\n\t\tif _, err := tempIn.Write(inCipherData); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mproc: failed to write input file: %w\", err)\n\t\t}\n\n\t\ttempIn.Close()\n\t}\n\tdefer os.Remove(tempIn.Name())\n\n\t// 准备临时输出文件\n\ttempOut, err := os.CreateTemp(\"\", \"certimate.mprocout_*.tmp\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mproc: failed to create temp output file: %w\", err)\n\t} else {\n\t\ttempOut.Close()\n\t}\n\tdefer os.Remove(tempOut.Name())\n\n\t// 准备临时错误文件\n\ttempErr, err := os.CreateTemp(\"\", \"certimate.mprocerr_*.tmp\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mproc: failed to create temp error file: %w\", err)\n\t} else {\n\t\ttempErr.Close()\n\t}\n\tdefer os.Remove(tempOut.Name())\n\n\t// 初始化子进程\n\tdone := make(chan struct{})\n\tmcmd := cmd.NewCmdOptions(cmd.Options{Buffered: false, Streaming: true},\n\t\ts.getEntrypoint(),\n\t\t\"intercmd\",\n\t\ts.command,\n\t\t\"--in\", tempIn.Name(),\n\t\t\"--out\", tempOut.Name(),\n\t\t\"--err\", tempErr.Name(),\n\t\t\"--enckey\", hex.EncodeToString(aesKey),\n\t)\n\tgo func() {\n\t\tdefer close(done)\n\t\tfor mcmd.Stdout != nil || mcmd.Stderr != nil {\n\t\t\tselect {\n\t\t\tcase line, open := <-mcmd.Stdout:\n\t\t\t\t{\n\t\t\t\t\tif !open {\n\t\t\t\t\t\tmcmd.Stdout = nil\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tif s.logger != nil {\n\t\t\t\t\t\tprint := s.logger.Info\n\n\t\t\t\t\t\t// split log level prefix for those vendor packages:\n\t\t\t\t\t\t// - github.com/go-acme/lego: INFO, WARN\n\t\t\t\t\t\tif strings.HasPrefix(line, \"[INFO] \") {\n\t\t\t\t\t\t\tline = strings.TrimPrefix(line, \"[INFO] \")\n\t\t\t\t\t\t\tprint = s.logger.Info\n\t\t\t\t\t\t} else if strings.HasPrefix(line, \"[WARN] \") {\n\t\t\t\t\t\t\tline = strings.TrimPrefix(line, \"[WARN] \")\n\t\t\t\t\t\t\tprint = s.logger.Warn\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tprint(line)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\tcase line, open := <-mcmd.Stderr:\n\t\t\t\t{\n\t\t\t\t\tif !open {\n\t\t\t\t\t\tmcmd.Stderr = nil\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tif s.logger != nil {\n\t\t\t\t\t\tprint := s.logger.Error\n\n\t\t\t\t\t\t// split log level prefix for those vendor packages:\n\t\t\t\t\t\t// - github.com/nrdcg/desec: DEBUG\n\t\t\t\t\t\tif strings.Contains(line, \"[DEBUG] \") {\n\t\t\t\t\t\t\tline = strings.SplitN(line, \"[DEBUG] \", 2)[1]\n\t\t\t\t\t\t\tprint = s.logger.Debug\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tprint(line)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\t// 等待子进程退出\n\t<-mcmd.Start()\n\t<-done\n\tif err := mcmd.Status().Error; err != nil {\n\t\treturn nil, fmt.Errorf(\"mproc: failed to exec child process: %w\", err)\n\t}\n\n\t// 读取输出\n\toutCipherData, err := os.ReadFile(tempOut.Name())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mproc: failed to read output file: %w\", err)\n\t} else {\n\t\terrData, _ := os.ReadFile(tempErr.Name())\n\t\tif len(errData) > 0 {\n\t\t\treturn nil, errors.New(string(errData))\n\t\t}\n\t}\n\n\t// 解密输出\n\toutPlainData, err := aesCryptor.CBCDecrypt(outCipherData)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mproc: failed to decrypt output data: %w\", err)\n\t}\n\n\t// 反序列化输出\n\tvar outData TOut\n\tif err := json.Unmarshal(outPlainData, &outData); err != nil {\n\t\treturn nil, fmt.Errorf(\"mproc: failed to unmarshal output data: %w\", err)\n\t}\n\n\treturn &outData, nil\n}\n\nfunc (s *sender[TIn, TOut]) getEntrypoint() string {\n\texecutable, err := os.Executable()\n\tif err != nil {\n\t\texecutable = os.Args[0]\n\t}\n\treturn executable\n}\n\n// 创建并返回一个多进程指令发送器。\n//\n// 入参:\n//   - command: 多进程指令命令。需要先注册为 `intercmd [command]` 命令行。\n//   - logger: 日志记录器，将重定向多进程的标准输出流和标准错误流到该日志记录器中。\n//\n// 出参:\n//   - 多进程指令发送器。\nfunc NewSender[TIn any, TOut any](command string, logger *slog.Logger) Sender[TIn, TOut] {\n\treturn &sender[TIn, TOut]{command: command, logger: logger}\n}\n"
  },
  {
    "path": "internal/tools/s3/client.go",
    "content": "﻿package s3\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/minio/minio-go/v7\"\n\t\"github.com/minio/minio-go/v7/pkg/credentials\"\n\t\"github.com/samber/lo\"\n\n\txhttp \"github.com/certimate-go/certimate/pkg/utils/http\"\n\txtls \"github.com/certimate-go/certimate/pkg/utils/tls\"\n)\n\ntype Client struct {\n\tcli *minio.Client\n}\n\nfunc NewClient(config *Config) (*Client, error) {\n\tif config == nil {\n\t\treturn nil, fmt.Errorf(\"the configuration of S3 client is nil\")\n\t}\n\n\tclient, err := createS3Client(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{cli: client}, nil\n}\n\nfunc (c *Client) PutObject(ctx context.Context, bucket, key string, reader io.Reader, size int64) error {\n\tputOpts := minio.PutObjectOptions{\n\t\tDisableMultipart: true,\n\t}\n\t_, err := c.cli.PutObject(ctx, bucket, key, reader, size, putOpts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"s3: failed to put object: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) PutObjectString(ctx context.Context, bucket, key string, data string) error {\n\treader := strings.NewReader(data)\n\treturn c.PutObject(ctx, bucket, key, reader, reader.Size())\n}\n\nfunc (c *Client) PutObjectBytes(ctx context.Context, bucket, key string, data []byte) error {\n\treader := bytes.NewReader(data)\n\treturn c.PutObject(ctx, bucket, key, reader, reader.Size())\n}\n\nfunc (c *Client) RemoveObject(ctx context.Context, bucket, key string) error {\n\tremoveOpts := minio.RemoveObjectOptions{}\n\terr := c.cli.RemoveObject(ctx, bucket, key, removeOpts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"s3: failed to remove object: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createS3Client(config *Config) (*minio.Client, error) {\n\tvar clientCred *credentials.Credentials\n\tswitch config.SignatureVersion {\n\tcase \"\", SignatureV4:\n\t\tclientCred = credentials.NewStaticV4(config.AccessKey, config.SecretKey, \"\")\n\tcase SignatureV2:\n\t\tclientCred = credentials.NewStaticV2(config.AccessKey, config.SecretKey, \"\")\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"s3: unsupported signature version: '%s'\", config.SignatureVersion)\n\t}\n\n\tendpoint, secure := resolveEndpoint(config.Endpoint)\n\tclientOpts := &minio.Options{\n\t\tCreds:        clientCred,\n\t\tRegion:       config.Region,\n\t\tBucketLookup: lo.If(config.UsePathStyle, minio.BucketLookupPath).Else(minio.BucketLookupDNS),\n\t\tSecure:       secure,\n\t}\n\n\tif secure && config.SkipTlsVerify {\n\t\ttransport := xhttp.NewDefaultTransport()\n\t\ttransport.TLSClientConfig = xtls.NewInsecureConfig()\n\t\tclientOpts.Transport = transport\n\t}\n\n\tclient, err := minio.New(endpoint, clientOpts)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"s3: %w\", err)\n\t}\n\n\treturn client, nil\n}\n\nfunc resolveEndpoint(endpoint string) (string, bool) {\n\tvar secure bool\n\tvar result string\n\n\treScheme := regexp.MustCompile(`^([^:]+)://`)\n\tif reScheme.MatchString(endpoint) {\n\t\ttemp := strings.Split(endpoint, \"://\")\n\t\tscheme := temp[0]\n\t\tresult = temp[1]\n\t\tsecure = strings.EqualFold(scheme, \"https\")\n\t} else {\n\t\tresult = endpoint\n\t\tsecure = true\n\t}\n\n\treturn result, secure\n}\n"
  },
  {
    "path": "internal/tools/s3/config.go",
    "content": "﻿package s3\n\nconst (\n\tSignatureV2 = \"v2\"\n\tSignatureV4 = \"v4\"\n)\n\nconst (\n\tdefaultSignatureVersion = SignatureV4\n)\n\ntype Config struct {\n\tEndpoint         string\n\tAccessKey        string\n\tSecretKey        string\n\tSignatureVersion string\n\tUsePathStyle     bool\n\tRegion           string\n\tSkipTlsVerify    bool\n}\n\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tSignatureVersion: defaultSignatureVersion,\n\t}\n}\n"
  },
  {
    "path": "internal/tools/smtp/client.go",
    "content": "﻿package smtp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/wneessen/go-mail\"\n\n\txtls \"github.com/certimate-go/certimate/pkg/utils/tls\"\n)\n\ntype Client struct {\n\tcli *mail.Client\n}\n\nfunc NewClient(config *Config) (*Client, error) {\n\tif config == nil {\n\t\treturn nil, fmt.Errorf(\"the configuration of SMTP client is nil\")\n\t}\n\n\tclient, err := createSmtpClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{cli: client}, nil\n}\n\nfunc (c *Client) Close() error {\n\treturn c.cli.Close()\n}\n\nfunc (c *Client) Send(ctx context.Context, msg *Message) error {\n\tif err := c.cli.DialAndSendWithContext(ctx, msg); err != nil {\n\t\terrShouldBeIgnored := false\n\n\t\t// REF: https://github.com/wneessen/go-mail/issues/463\n\t\tvar sendErr *mail.SendError\n\t\tif errors.As(err, &sendErr) {\n\t\t\tif sendErr.Reason == mail.ErrSMTPReset {\n\t\t\t\terrShouldBeIgnored = true\n\t\t\t}\n\t\t}\n\n\t\tif !errShouldBeIgnored {\n\t\t\treturn fmt.Errorf(\"smtp: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc createSmtpClient(config *Config) (*mail.Client, error) {\n\tclientOptions := []mail.Option{\n\t\tmail.WithSMTPAuth(mail.SMTPAuthAutoDiscover),\n\t\tmail.WithUsername(config.Username),\n\t\tmail.WithPassword(config.Password),\n\t\tmail.WithTimeout(time.Second * 30),\n\t}\n\n\tif config.Port == 0 {\n\t\tif config.UseSsl {\n\t\t\tclientOptions = append(clientOptions, mail.WithPort(mail.DefaultPortSSL))\n\t\t} else {\n\t\t\tclientOptions = append(clientOptions, mail.WithPort(mail.DefaultPort))\n\t\t}\n\t} else {\n\t\tclientOptions = append(clientOptions, mail.WithPort(config.Port))\n\t}\n\n\tif config.UseSsl {\n\t\ttlsConfig := xtls.NewCompatibleConfig()\n\t\tif config.SkipTlsVerify {\n\t\t\ttlsConfig.InsecureSkipVerify = true\n\t\t} else {\n\t\t\ttlsConfig.ServerName = config.Host\n\t\t}\n\n\t\tclientOptions = append(clientOptions, mail.WithSSL())\n\t\tclientOptions = append(clientOptions, mail.WithTLSConfig(tlsConfig))\n\t\tclientOptions = append(clientOptions, mail.WithTLSPolicy(mail.TLSMandatory))\n\t} else {\n\t\tclientOptions = append(clientOptions, mail.WithTLSPolicy(mail.TLSOpportunistic))\n\t}\n\n\tclient, err := mail.NewClient(config.Host, clientOptions...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"smtp: %w\", err)\n\t}\n\n\tclient.ErrorHandlerRegistry.RegisterHandler(\"smtp.qq.com\", \"QUIT\", &wQQMailQuitErrorHandler{})\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "internal/tools/smtp/config.go",
    "content": "﻿package smtp\n\nconst (\n\tdefaultPort int = 25\n)\n\ntype Config struct {\n\tHost          string\n\tPort          int\n\tUsername      string\n\tPassword      string\n\tUseSsl        bool\n\tSkipTlsVerify bool\n}\n\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPort: defaultPort,\n\t}\n}\n"
  },
  {
    "path": "internal/tools/smtp/errhandler.go",
    "content": "package smtp\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"io\"\n\t\"net/textproto\"\n)\n\n// REF: https://github.com/wneessen/go-mail/wiki/Error-Registry\ntype wQQMailQuitErrorHandler struct{}\n\nfunc (q *wQQMailQuitErrorHandler) HandleError(_, _ string, conn *textproto.Conn, err error) error {\n\tvar tpErr textproto.ProtocolError\n\tif errors.As(err, &tpErr) {\n\t\tif len(tpErr.Error()) < 16 {\n\t\t\treturn err\n\t\t}\n\t\tif !bytes.Equal([]byte(tpErr.Error()[16:]), []byte(\"\\x00\\x00\\x00\\x1a\\x00\\x00\\x00\")) {\n\t\t\treturn err\n\t\t}\n\t\t_, _ = io.ReadFull(conn.R, make([]byte, 8))\n\t\treturn nil\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "internal/tools/smtp/message.go",
    "content": "﻿package smtp\n\nimport (\n\t\"github.com/wneessen/go-mail\"\n)\n\ntype Message = mail.Msg\n\nfunc NewMessage() *Message {\n\treturn mail.NewMsg()\n}\n\ntype MIMEType = mail.ContentType\n\nconst (\n\tMIMETypeTextHTML  MIMEType = mail.TypeTextHTML\n\tMIMETypeTextPlain MIMEType = mail.TypeTextPlain\n)\n"
  },
  {
    "path": "internal/tools/ssh/auth.go",
    "content": "package ssh\n\ntype AuthMethodType string\n\nconst (\n\tAuthMethodTypeNone     AuthMethodType = \"none\"\n\tAuthMethodTypePassword AuthMethodType = \"password\"\n\tAuthMethodTypeKey      AuthMethodType = \"key\"\n)\n"
  },
  {
    "path": "internal/tools/ssh/client.go",
    "content": "package ssh\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\ntype Client struct {\n\tconns []net.Conn\n\tclis  []*ssh.Client\n}\n\nfunc NewClient(config *Config) (*Client, error) {\n\tif config == nil {\n\t\treturn nil, fmt.Errorf(\"the configuration of SSH client is nil\")\n\t}\n\n\tconns, clis, err := createConnsAndSshClients(config)\n\tif err != nil {\n\t\tfor i := len(clis) - 1; i >= 0; i-- {\n\t\t\tclis[i].Close()\n\t\t}\n\n\t\tfor i := len(conns) - 1; i >= 0; i-- {\n\t\t\tconns[i].Close()\n\t\t}\n\n\t\treturn nil, err\n\t}\n\n\treturn &Client{conns: conns, clis: clis}, nil\n}\n\nfunc (c *Client) Close() error {\n\terrs := make([]error, 0)\n\n\tfor i := len(c.clis) - 1; i >= 0; i-- {\n\t\tcli := c.clis[i]\n\t\tif err := cli.Close(); err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\n\tfor i := len(c.conns) - 1; i >= 0; i-- {\n\t\tconn := c.conns[i]\n\t\tif err := conn.Close(); err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\n\tif len(errs) == 0 {\n\t\treturn nil\n\t} else if len(errs) == 1 {\n\t\treturn errs[0]\n\t} else {\n\t\treturn errors.Join(errs...)\n\t}\n}\n\nfunc (c *Client) GetClient() *ssh.Client {\n\tif len(c.clis) == 0 {\n\t\treturn nil\n\t}\n\n\treturn c.clis[len(c.clis)-1]\n}\n\nfunc createConnsAndSshClients(config *Config) (conns []net.Conn, clis []*ssh.Client, err error) {\n\tconns = make([]net.Conn, 0)\n\tclis = make([]*ssh.Client, 0)\n\n\tvar targetConn net.Conn\n\tif len(config.JumpServers) > 0 {\n\t\tvar jumpCli *ssh.Client\n\n\t\tfor i, jumpConfig := range config.JumpServers {\n\t\t\tvar jumpConn net.Conn\n\t\t\tif jumpCli == nil {\n\t\t\t\tjumpConn, err = net.Dial(\"tcp\", resolveAddr(jumpConfig.Host, jumpConfig.Port))\n\t\t\t} else {\n\t\t\t\tjumpConn, err = jumpCli.Dial(\"tcp\", resolveAddr(jumpConfig.Host, jumpConfig.Port))\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\terr = fmt.Errorf(\"ssh: failed to connect to jump server [%d]: %w\", i+1, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconns = append(conns, jumpConn)\n\n\t\t\tjumpCli, err = createSshClientWithConn(&jumpConfig, jumpConn)\n\t\t\tif err != nil {\n\t\t\t\terr = fmt.Errorf(\"ssh: failed to create jump server SSH client[%d]: %w\", i+1, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tclis = append(clis, jumpCli)\n\t\t}\n\n\t\t// 通过跳板机发起 TCP 连接到目标服务器\n\t\ttargetConn, err = jumpCli.Dial(\"tcp\", resolveAddr(config.Host, config.Port))\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"ssh: failed to connect to target server: %w\", err)\n\t\t\treturn\n\t\t}\n\n\t\tconns = append(conns, targetConn)\n\t} else {\n\t\t// 直接发起 TCP 连接到目标服务器\n\t\ttargetConn, err = net.Dial(\"tcp\", resolveAddr(config.Host, config.Port))\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"ssh: failed to connect to target server: %w\", err)\n\t\t\treturn\n\t\t}\n\n\t\tconns = append(conns, targetConn)\n\t}\n\n\t// 创建 SSH 客户端\n\ttargetCli, err := createSshClientWithConn(&config.ServerConfig, targetConn)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"ssh: failed to create SSH client: %w\", err)\n\t}\n\n\tclis = append(clis, targetCli)\n\n\treturn conns, clis, nil\n}\n\nfunc createSshClientWithConn(config *ServerConfig, conn net.Conn) (*ssh.Client, error) {\n\tif conn == nil {\n\t\treturn nil, fmt.Errorf(\"ssh: nil conn\")\n\t}\n\n\tauthMethodType := lo.\n\t\tIf(string(config.AuthMethod) != \"\", config.AuthMethod).\n\t\tElseIf(config.Key != \"\", AuthMethodTypeKey).\n\t\tElseIf(config.Password != \"\", AuthMethodTypePassword).\n\t\tElse(AuthMethodTypeNone)\n\tauthMethods := make([]ssh.AuthMethod, 0)\n\tswitch authMethodType {\n\tcase AuthMethodTypeNone:\n\t\t{\n\t\t\tif config.Username == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"ssh: unset username\")\n\t\t\t}\n\t\t}\n\n\tcase AuthMethodTypePassword:\n\t\t{\n\t\t\tif config.Username == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"ssh: unset username\")\n\t\t\t}\n\t\t\tif config.Password == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"ssh: unset password\")\n\t\t\t}\n\n\t\t\tpassword := config.Password\n\t\t\tauthMethods = append(authMethods, ssh.Password(password))\n\t\t\tauthMethods = append(authMethods, ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) {\n\t\t\t\tanswers := make([]string, len(questions))\n\t\t\t\tif len(answers) == 0 {\n\t\t\t\t\treturn answers, nil\n\t\t\t\t}\n\n\t\t\t\tfor i, question := range questions {\n\t\t\t\t\tquestion = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(question), \":\"))\n\t\t\t\t\tif strings.EqualFold(question, \"Password\") {\n\t\t\t\t\t\tanswers[i] = password\n\t\t\t\t\t\treturn answers, nil\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn nil, fmt.Errorf(\"unexpected keyboard interactive question '%s'\", strings.Join(questions, \", \"))\n\t\t\t}))\n\t\t}\n\n\tcase AuthMethodTypeKey:\n\t\t{\n\t\t\tif config.Username == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"ssh: unset username\")\n\t\t\t}\n\t\t\tif config.Key == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"ssh: unset key\")\n\t\t\t}\n\n\t\t\tkey := config.Key\n\t\t\tkeyPassphrase := config.KeyPassphrase\n\n\t\t\tvar signer ssh.Signer\n\t\t\tvar err error\n\t\t\tif keyPassphrase != \"\" {\n\t\t\t\tsigner, err = ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(keyPassphrase))\n\t\t\t} else {\n\t\t\t\tsigner, err = ssh.ParsePrivateKey([]byte(key))\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"ssh: %w\", err)\n\t\t\t}\n\n\t\t\tauthMethods = append(authMethods, ssh.PublicKeys(signer))\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"ssh: unsupported auth method '%s'\", authMethodType)\n\t}\n\n\taddr := resolveAddr(config.Host, config.Port)\n\tsshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, &ssh.ClientConfig{\n\t\tUser:            config.Username,\n\t\tAuth:            authMethods,\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ssh: %w\", err)\n\t}\n\n\treturn ssh.NewClient(sshConn, chans, reqs), nil\n}\n\nfunc resolveAddr(host string, port int) string {\n\tif port == 0 {\n\t\tport = defaultPort\n\t}\n\treturn net.JoinHostPort(host, strconv.Itoa(port))\n}\n"
  },
  {
    "path": "internal/tools/ssh/config.go",
    "content": "package ssh\n\nconst (\n\tdefaultPort       int            = 22\n\tdefaultAuthMethod AuthMethodType = AuthMethodTypeNone\n\tdefaultUsername   string         = \"root\"\n)\n\ntype ServerConfig struct {\n\tHost          string\n\tPort          int\n\tAuthMethod    AuthMethodType\n\tUsername      string\n\tPassword      string\n\tKey           string\n\tKeyPassphrase string\n}\n\ntype Config struct {\n\tServerConfig\n\tJumpServers []ServerConfig\n}\n\nfunc NewServerConfig() *ServerConfig {\n\treturn &ServerConfig{\n\t\tPort:       defaultPort,\n\t\tAuthMethod: defaultAuthMethod,\n\t\tUsername:   defaultUsername,\n\t}\n}\n\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tServerConfig: *NewServerConfig(),\n\t}\n}\n"
  },
  {
    "path": "internal/workflow/dispatcher/deps.go",
    "content": "package dispatcher\n\nimport (\n\t\"context\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\ntype workflowRepository interface {\n\tGetById(ctx context.Context, id string) (*domain.Workflow, error)\n\tSave(ctx context.Context, workflow *domain.Workflow) (*domain.Workflow, error)\n}\n\ntype workflowRunRepository interface {\n\tGetById(ctx context.Context, id string) (*domain.WorkflowRun, error)\n\tSave(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error)\n\tSaveWithCascading(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error)\n}\n\ntype workflowLogRepository interface {\n\tSave(ctx context.Context, workflowLog *domain.WorkflowLog) (*domain.WorkflowLog, error)\n}\n"
  },
  {
    "path": "internal/workflow/dispatcher/dispatcher.go",
    "content": "package dispatcher\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"log/slog\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/internal/repository\"\n\t\"github.com/certimate-go/certimate/internal/workflow/engine\"\n\t\"github.com/certimate-go/certimate/pkg/logging\"\n\txenv \"github.com/certimate-go/certimate/pkg/utils/env\"\n)\n\nvar envMaxWorkers = 1\n\nfunc init() {\n\tenvMaxWorkers = xenv.GetOrDefaultInt(\"CERTIMATE_WORKFLOW_MAX_WORKERS\", runtime.GOMAXPROCS(0))\n\tif envMaxWorkers <= 0 {\n\t\tenvMaxWorkers = max(1, runtime.NumCPU())\n\t}\n}\n\ntype WorkflowDispatcher interface {\n\tGetStatistics() Statistics\n\n\tBootup(ctx context.Context) error\n\tShutdown(ctx context.Context) error\n\tStart(ctx context.Context, runId string) error\n\tCancel(ctx context.Context, runId string) error\n}\n\ntype Statistics struct {\n\tConcurrency      int\n\tPendingRunIds    []string\n\tProcessingRunIds []string\n}\n\ntype workflowDispatcher struct {\n\tbooted      bool\n\tconcurrency int\n\n\ttaskMtx         sync.RWMutex\n\tpendingRunQueue []string\n\tprocessingTasks map[string]*taskInfo // Key: RunId\n\n\tworkflowRepo    workflowRepository\n\tworkflowRunRepo workflowRunRepository\n\tworkflowLogRepo workflowLogRepository\n\n\tsyslog *slog.Logger\n}\n\nvar _ WorkflowDispatcher = (*workflowDispatcher)(nil)\n\nfunc (wd *workflowDispatcher) GetStatistics() Statistics {\n\twd.taskMtx.RLock()\n\tdefer wd.taskMtx.RUnlock()\n\n\tstats := Statistics{\n\t\tConcurrency:      wd.concurrency,\n\t\tPendingRunIds:    make([]string, 0),\n\t\tProcessingRunIds: make([]string, 0),\n\t}\n\tfor _, pendingRunId := range wd.pendingRunQueue {\n\t\tstats.PendingRunIds = append(stats.PendingRunIds, pendingRunId)\n\t}\n\tfor _, processingRunId := range wd.processingTasks {\n\t\tstats.ProcessingRunIds = append(stats.ProcessingRunIds, processingRunId.RunId)\n\t}\n\n\treturn stats\n}\n\nfunc (wd *workflowDispatcher) Bootup(ctx context.Context) error {\n\tif wd.booted {\n\t\treturn errors.New(\"could not re-bootup\")\n\t}\n\n\twd.taskMtx.Lock()\n\tdefer wd.taskMtx.Unlock()\n\n\tif _, err := app.GetDB().NewQuery(fmt.Sprintf(\"UPDATE %s SET lastRunStatus = 'canceled' WHERE lastRunStatus = 'pending' OR lastRunStatus = 'processing'\", domain.CollectionNameWorkflow)).Execute(); err != nil {\n\t\treturn err\n\t}\n\tif _, err := app.GetDB().NewQuery(fmt.Sprintf(\"UPDATE %s SET status = 'canceled' WHERE status = 'pending' OR status = 'processing'\", domain.CollectionNameWorkflowRun)).Execute(); err != nil {\n\t\treturn err\n\t}\n\n\twd.booted = true\n\n\treturn nil\n}\n\nfunc (wd *workflowDispatcher) Shutdown(ctx context.Context) error {\n\tif !wd.booted {\n\t\treturn errors.New(\"could not re-shutdown\")\n\t}\n\n\twd.taskMtx.Lock()\n\tdefer wd.taskMtx.Unlock()\n\n\tfor runId, task := range wd.processingTasks {\n\t\ttask.cancel()\n\t\tdelete(wd.processingTasks, runId)\n\t}\n\n\twd.booted = false\n\twd.pendingRunQueue = make([]string, 0)\n\twd.processingTasks = make(map[string]*taskInfo)\n\treturn nil\n}\n\nfunc (wd *workflowDispatcher) Start(ctx context.Context, runId string) error {\n\twd.taskMtx.Lock()\n\tdefer wd.taskMtx.Unlock()\n\n\tif _, exists := wd.processingTasks[runId]; exists {\n\t\treturn fmt.Errorf(\"workflow run %s is already processing\", runId)\n\t}\n\n\tfor _, pendingRunId := range wd.pendingRunQueue {\n\t\tif pendingRunId == runId {\n\t\t\treturn fmt.Errorf(\"workflow run %s is already in the queue\", runId)\n\t\t}\n\t}\n\n\twd.pendingRunQueue = append(wd.pendingRunQueue, runId)\n\tgo func() { wd.tryNextAsync() }()\n\n\treturn nil\n}\n\nfunc (wd *workflowDispatcher) Cancel(ctx context.Context, runId string) error {\n\twd.taskMtx.Lock()\n\tdefer wd.taskMtx.Unlock()\n\n\tworkflowRun, err := wd.workflowRunRepo.GetById(ctx, runId)\n\tif err != nil {\n\t\treturn err\n\t} else if workflowRun.Status != domain.WorkflowRunStatusTypePending && workflowRun.Status != domain.WorkflowRunStatusTypeProcessing {\n\t\treturn fmt.Errorf(\"workrun #%s is already completed\", workflowRun.Id)\n\t}\n\n\tworkflow, err := wd.workflowRepo.GetById(ctx, workflowRun.WorkflowId)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tworkflowRun.Status = domain.WorkflowRunStatusTypeCanceled\n\tif workflow.LastRunId == workflowRun.Id {\n\t\t_, err := wd.workflowRunRepo.SaveWithCascading(ctx, workflowRun)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\t_, err := wd.workflowRunRepo.Save(ctx, workflowRun)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif task, exists := wd.processingTasks[runId]; exists {\n\t\ttask.cancel()\n\t\tdelete(wd.processingTasks, runId)\n\n\t\twd.syslog.Info(fmt.Sprintf(\"workrun #%s was canceled\", task.RunId))\n\t}\n\n\tfor i, pendingRunId := range wd.pendingRunQueue {\n\t\tif pendingRunId == runId {\n\t\t\twd.pendingRunQueue = append(wd.pendingRunQueue[:i], wd.pendingRunQueue[i+1:]...)\n\t\t\tbreak\n\t\t}\n\t}\n\n\tgo func() { wd.tryNextAsync() }()\n\n\treturn nil\n}\n\nfunc (wd *workflowDispatcher) tryExecuteAsync(task *taskInfo) {\n\tvar workflow *domain.Workflow\n\tvar workflowRun *domain.WorkflowRun\n\tvar err error\n\n\t// 捕获 panic\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\twd.syslog.Error(fmt.Sprintf(\"workflow dispatcher panic: %v\", r), slog.String(\"workflowId\", task.WorkflowId), slog.String(\"runId\", task.RunId))\n\t\t\tslog.Error(fmt.Sprintf(\"workflow dispatcher panic: %v, stack trace: %s\", r, string(debug.Stack())), slog.String(\"workflowId\", task.WorkflowId), slog.String(\"runId\", task.RunId))\n\n\t\t\tif workflowRun != nil {\n\t\t\t\tworkflowRun.Status = domain.WorkflowRunStatusTypeFailed\n\t\t\t\tworkflowRun.EndedAt = time.Now()\n\t\t\t\tworkflowRun.Error = fmt.Sprintf(\"workflow dispatcher panic: %v\", r)\n\t\t\t\tif _, err := wd.workflowRunRepo.SaveWithCascading(context.Background(), workflowRun); err != nil {\n\t\t\t\t\tlog.Default().Println(\"failed to save workflow run after panic\", slog.Any(\"error\", err))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\t// 尝试继续执行等待队列中的任务\n\tdefer func() {\n\t\twd.taskMtx.Lock()\n\t\tdelete(wd.processingTasks, task.RunId)\n\t\twd.taskMtx.Unlock()\n\n\t\tgo func() { wd.tryNextAsync() }()\n\t}()\n\n\t// 查询运行实体，并级联更新状态\n\tif workflowRun, err = wd.workflowRunRepo.GetById(task.ctx, task.RunId); err != nil {\n\t\tif !(errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {\n\t\t\twd.syslog.Error(fmt.Sprintf(\"failed to get workrun #%s record\", task.RunId), slog.Any(\"error\", err))\n\t\t}\n\t\treturn\n\t} else {\n\t\tif workflowRun.Status == domain.WorkflowRunStatusTypePending {\n\t\t\tworkflowRun.Status = domain.WorkflowRunStatusTypeProcessing\n\t\t\twd.workflowRunRepo.SaveWithCascading(task.ctx, workflowRun)\n\t\t} else {\n\t\t\t// WTF? That should be impossible!\n\t\t\treturn\n\t\t}\n\t}\n\n\t// 查询工作流实体\n\tworkflow, err = wd.workflowRepo.GetById(task.ctx, workflowRun.WorkflowId)\n\tif err != nil {\n\t\twd.syslog.Error(fmt.Sprintf(\"failed to get workflow #%s record\", workflowRun.WorkflowId), slog.Any(\"error\", err))\n\t\treturn\n\t}\n\n\t// 初始化工作流引擎\n\tlogsBuf := make(domain.WorkflowLogs, 0)\n\twe := engine.NewWorkflowEngine()\n\twe.OnEnd(func(ctx context.Context) error {\n\t\tif errmsg := logsBuf.ErrorString(); errmsg == \"\" {\n\t\t\tworkflowRun.Status = domain.WorkflowRunStatusTypeSucceeded\n\t\t\tworkflowRun.EndedAt = time.Now()\n\t\t} else {\n\t\t\tworkflowRun.Status = domain.WorkflowRunStatusTypeFailed\n\t\t\tworkflowRun.EndedAt = time.Now()\n\t\t\tworkflowRun.Error = errmsg\n\t\t}\n\t\twd.workflowRunRepo.SaveWithCascading(task.ctx, workflowRun)\n\n\t\treturn nil\n\t})\n\twe.OnError(func(ctx context.Context, err error) error {\n\t\tif errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {\n\t\t\tworkflowRun.Status = domain.WorkflowRunStatusTypeCanceled\n\t\t\twd.workflowRunRepo.SaveWithCascading(context.Background(), workflowRun)\n\t\t} else {\n\t\t\tworkflowRun.Status = domain.WorkflowRunStatusTypeFailed\n\t\t\tworkflowRun.EndedAt = time.Now()\n\t\t\tworkflowRun.Error = err.Error()\n\t\t\twd.workflowRunRepo.SaveWithCascading(task.ctx, workflowRun)\n\t\t}\n\n\t\treturn nil\n\t})\n\twe.OnNodeError(func(ctx context.Context, node *engine.Node, err error) error {\n\t\tif errors.Is(err, engine.ErrTerminated) || errors.Is(err, engine.ErrBlocksException) {\n\t\t\treturn nil\n\t\t}\n\n\t\tlog := domain.WorkflowLog{}\n\t\tlog.WorkflowId = task.WorkflowId\n\t\tlog.RunId = task.RunId\n\t\tlog.NodeId = node.Id\n\t\tlog.NodeName = node.Data.Name\n\t\tlog.TimestampMilli = time.Now().UnixMilli()\n\t\tlog.Level = int32(slog.LevelError)\n\t\tlog.Message = err.Error()\n\t\tlog.CreatedAt = time.Now()\n\t\tlogsBuf = append(logsBuf, log)\n\n\t\tif _, err := wd.workflowLogRepo.Save(ctx, &log); err != nil {\n\t\t\twd.syslog.Error(err.Error())\n\t\t}\n\n\t\treturn nil\n\t})\n\twe.OnNodeLogging(func(ctx context.Context, node *engine.Node, record logging.Record) error {\n\t\tlog := domain.WorkflowLog{}\n\t\tlog.WorkflowId = task.WorkflowId\n\t\tlog.RunId = task.RunId\n\t\tlog.NodeId = node.Id\n\t\tlog.NodeName = node.Data.Name\n\t\tlog.TimestampMilli = record.Time.UnixMilli()\n\t\tlog.Level = int32(record.Level)\n\t\tlog.Message = record.Message\n\t\tlog.Data = record.Data()\n\t\tlog.CreatedAt = time.Now()\n\t\tlogsBuf = append(logsBuf, log)\n\n\t\tif _, err := wd.workflowLogRepo.Save(ctx, &log); err != nil {\n\t\t\twd.syslog.Error(err.Error())\n\t\t}\n\n\t\treturn nil\n\t})\n\n\t// 执行工作流\n\twd.syslog.Info(fmt.Sprintf(\"workflow #%s's run #%s started\", task.WorkflowId, task.RunId))\n\twe.Invoke(task.ctx, engine.WorkflowExecution{\n\t\tWorkflowId:   workflowRun.WorkflowId,\n\t\tWorkflowName: workflow.Name,\n\t\tRunId:        workflowRun.Id,\n\t\tRunTrigger:   workflowRun.Trigger,\n\t\tGraph:        workflowRun.Graph,\n\t})\n\twd.syslog.Info(fmt.Sprintf(\"workflow #%s's run #%s stopped\", task.WorkflowId, task.RunId))\n}\n\nfunc (wd *workflowDispatcher) tryNextAsync() {\n\twd.taskMtx.RLock()\n\n\tfor _, pendingRunId := range wd.pendingRunQueue {\n\t\tworkflowRun, err := wd.workflowRunRepo.GetById(context.Background(), pendingRunId)\n\t\tif err != nil {\n\t\t\twd.syslog.Error(fmt.Sprintf(\"failed to get workrun #%s record\", pendingRunId), slog.Any(\"error\", err))\n\t\t\tcontinue\n\t\t}\n\n\t\tvar hasSameWorkflowTask bool // 相同 Workflow 的任务同一时间只能有一个 Run 在执行\n\t\tfor _, processingTask := range wd.processingTasks {\n\t\t\tif processingTask.WorkflowId == workflowRun.WorkflowId {\n\t\t\t\thasSameWorkflowTask = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif hasSameWorkflowTask {\n\t\t\twd.syslog.Warn(fmt.Sprintf(\"workflow #%s's run #%s is pending, because tasks that belonging to the same workflow already exists\", workflowRun.WorkflowId, workflowRun.Id))\n\t\t} else if len(wd.processingTasks) >= wd.concurrency && wd.concurrency > 0 {\n\t\t\twd.syslog.Warn(fmt.Sprintf(\"workflow #%s's run #%s is pending, because the maximum concurrency (limit: %d) has been reached\", workflowRun.WorkflowId, workflowRun.Id, wd.concurrency))\n\t\t} else {\n\t\t\twd.taskMtx.RUnlock()\n\n\t\t\twd.taskMtx.Lock()\n\t\t\tctxRun, ctxCancel := context.WithCancel(context.Background())\n\t\t\ttask := &taskInfo{WorkflowId: workflowRun.WorkflowId, RunId: workflowRun.Id, ctx: ctxRun, cancel: ctxCancel}\n\t\t\twd.pendingRunQueue = lo.Filter(wd.pendingRunQueue, func(s string, _ int) bool { return s != pendingRunId })\n\t\t\twd.processingTasks[pendingRunId] = task\n\t\t\twd.syslog.Info(fmt.Sprintf(\"workflow #%s's run #%s is being dispatched ...\", task.WorkflowId, task.RunId))\n\t\t\twd.taskMtx.Unlock()\n\n\t\t\tgo func() { wd.tryExecuteAsync(task) }()\n\t\t\treturn\n\t\t}\n\t}\n\n\twd.taskMtx.RUnlock()\n}\n\nfunc newWorkflowDispatcher() WorkflowDispatcher {\n\treturn &workflowDispatcher{\n\t\tconcurrency: envMaxWorkers,\n\n\t\tpendingRunQueue: make([]string, 0),\n\t\tprocessingTasks: make(map[string]*taskInfo),\n\n\t\tworkflowRepo:    repository.NewWorkflowRepository(),\n\t\tworkflowRunRepo: repository.NewWorkflowRunRepository(),\n\t\tworkflowLogRepo: repository.NewWorkflowLogRepository(),\n\n\t\tsyslog: app.GetLogger(),\n\t}\n}\n"
  },
  {
    "path": "internal/workflow/dispatcher/singleton.go",
    "content": "package dispatcher\n\nimport (\n\t\"sync\"\n)\n\nvar (\n\tinstance    WorkflowDispatcher\n\tintanceOnce sync.Once\n)\n\nfunc GetSingletonDispatcher() WorkflowDispatcher {\n\tintanceOnce.Do(func() {\n\t\tinstance = newWorkflowDispatcher()\n\t})\n\treturn instance\n}\n"
  },
  {
    "path": "internal/workflow/dispatcher/task.go",
    "content": "package dispatcher\n\nimport (\n\t\"context\"\n)\n\ntype taskInfo struct {\n\tWorkflowId string\n\tRunId      string\n\n\tctx    context.Context\n\tcancel context.CancelFunc\n}\n"
  },
  {
    "path": "internal/workflow/engine/context.go",
    "content": "package engine\n\nimport (\n\t\"context\"\n)\n\ntype WorkflowContext struct {\n\tWorkflowId string\n\tRunId      string\n\tRunGraph   *Graph\n\n\tengine    WorkflowEngine\n\tvariables VariableManager\n\tinputs    InOutManager\n\n\tctx context.Context\n}\n\nfunc (c *WorkflowContext) SetExecutingWorkflow(workflowId string, runId string, runGraph *Graph) *WorkflowContext {\n\tc.WorkflowId = workflowId\n\tc.RunId = runId\n\tc.RunGraph = runGraph\n\treturn c\n}\n\nfunc (c *WorkflowContext) SetEngine(engine WorkflowEngine) *WorkflowContext {\n\tc.engine = engine\n\treturn c\n}\n\nfunc (c *WorkflowContext) SetVariablesManager(inputs VariableManager) *WorkflowContext {\n\tc.variables = inputs\n\treturn c\n}\n\nfunc (c *WorkflowContext) SetInputsManager(manager InOutManager) *WorkflowContext {\n\tc.inputs = manager\n\treturn c\n}\n\nfunc (c *WorkflowContext) SetContext(ctx context.Context) *WorkflowContext {\n\tc.ctx = ctx\n\treturn c\n}\n\nfunc (c *WorkflowContext) Context() context.Context {\n\treturn c.ctx\n}\n\nfunc (c *WorkflowContext) Clone() *WorkflowContext {\n\treturn &WorkflowContext{\n\t\tWorkflowId: c.WorkflowId,\n\t\tRunId:      c.RunId,\n\t\tRunGraph:   c.RunGraph,\n\n\t\tengine:    c.engine,\n\t\tvariables: c.variables,\n\t\tinputs:    c.inputs,\n\n\t\tctx: c.ctx,\n\t}\n}\n"
  },
  {
    "path": "internal/workflow/engine/deps.go",
    "content": "package engine\n\nimport (\n\t\"context\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\ntype accessRepository interface {\n\tGetById(ctx context.Context, id string) (*domain.Access, error)\n}\n\ntype certificateRepository interface {\n\tGetById(ctx context.Context, id string) (*domain.Certificate, error)\n\tGetByWorkflowRunIdAndNodeId(ctx context.Context, workflowRunId string, workflowNodeId string) (*domain.Certificate, error)\n\tSave(ctx context.Context, certificate *domain.Certificate) (*domain.Certificate, error)\n}\n\ntype workflowOutputRepository interface {\n\tGetByWorkflowIdAndNodeId(ctx context.Context, workflowId string, workflowNodeId string) (*domain.WorkflowOutput, error)\n\tSave(ctx context.Context, workflowOutput *domain.WorkflowOutput) (*domain.WorkflowOutput, error)\n}\n\ntype settingsRepository interface {\n\tGetByName(ctx context.Context, name string) (*domain.Settings, error)\n}\n"
  },
  {
    "path": "internal/workflow/engine/engine.go",
    "content": "package engine\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"runtime/debug\"\n\t\"sync\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/internal/repository\"\n\t\"github.com/certimate-go/certimate/pkg/logging\"\n)\n\ntype WorkflowExecution struct {\n\tWorkflowId   string\n\tWorkflowName string\n\tRunId        string\n\tRunTrigger   domain.WorkflowTriggerType\n\tGraph        *Graph\n}\n\ntype WorkflowEngine interface {\n\tInvoke(ctx context.Context, execution WorkflowExecution) error\n\n\tOnStart(callback func(ctx context.Context) error)\n\tOnEnd(callback func(ctx context.Context) error)\n\tOnError(callback func(ctx context.Context, err error) error)\n\tOnNodeStart(callback func(ctx context.Context, node *Node) error)\n\tOnNodeEnd(callback func(ctx context.Context, node *Node, res *NodeExecutionResult) error)\n\tOnNodeError(callback func(ctx context.Context, node *Node, err error) error)\n\tOnNodeLogging(callback func(ctx context.Context, node *Node, log logging.Record) error)\n}\n\ntype workflowEngine struct {\n\texecutors map[NodeType]NodeExecutor\n\n\thooksMtx           sync.RWMutex\n\tonStartHooks       [](func(ctx context.Context) error)\n\tonEndHooks         [](func(ctx context.Context) error)\n\tonErrorHooks       [](func(ctx context.Context, err error) error)\n\tonNodeStartHooks   [](func(ctx context.Context, node *Node) error)\n\tonNodeEndHooks     [](func(ctx context.Context, node *Node, res *NodeExecutionResult) error)\n\tonNodeErrorHooks   [](func(ctx context.Context, node *Node, err error) error)\n\tonNodeLoggingHooks [](func(ctx context.Context, node *Node, log logging.Record) error)\n\n\twfoutputRepo workflowOutputRepository\n\n\tsyslog *slog.Logger\n}\n\nvar _ WorkflowEngine = (*workflowEngine)(nil)\n\nfunc (we *workflowEngine) Invoke(ctx context.Context, execution WorkflowExecution) error {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\twe.fireOnErrorHooks(ctx, fmt.Errorf(\"workflow engine panic: %v\", r))\n\t\t\twe.syslog.Error(fmt.Sprintf(\"workflow engine panic: %v\", r), slog.String(\"workflowId\", execution.WorkflowId), slog.String(\"runId\", execution.RunId))\n\t\t\tslog.Error(fmt.Sprintf(\"workflow engine panic: %v, stack trace: %s\", r, string(debug.Stack())), slog.String(\"workflowId\", execution.WorkflowId), slog.String(\"runId\", execution.RunId))\n\t\t}\n\t}()\n\n\twe.fireOnStartHooks(ctx)\n\n\twfIOs := newInOutManager()\n\n\twfVars := newVariableManager()\n\twfVars.Set(stateVarKeyWorkflowId, execution.WorkflowId, stateValTypeString)\n\twfVars.Set(stateVarKeyWorkflowName, execution.WorkflowName, stateValTypeString)\n\twfVars.Set(stateVarKeyRunId, execution.RunId, stateValTypeString)\n\twfVars.Set(stateVarKeyRunTrigger, execution.RunTrigger, stateValTypeString)\n\twfVars.Set(stateVarKeyErrorNodeId, \"\", stateValTypeString)\n\twfVars.Set(stateVarKeyErrorNodeName, \"\", stateValTypeString)\n\twfVars.Set(stateVarKeyErrorMessage, \"\", stateValTypeString)\n\n\twfCtx := (&WorkflowContext{}).\n\t\tSetExecutingWorkflow(execution.WorkflowId, execution.RunId, execution.Graph).\n\t\tSetEngine(we).\n\t\tSetInputsManager(wfIOs).\n\t\tSetVariablesManager(wfVars).\n\t\tSetContext(ctx)\n\tif err := we.executeBlocks(wfCtx, execution.Graph.Nodes); err != nil {\n\t\tif !errors.Is(err, ErrTerminated) {\n\t\t\twe.fireOnErrorHooks(ctx, err)\n\t\t\treturn err\n\t\t}\n\t}\n\n\twe.fireOnEndHooks(ctx)\n\n\treturn nil\n}\n\nfunc (we *workflowEngine) OnStart(callback func(ctx context.Context) error) {\n\twe.hooksMtx.Lock()\n\tdefer we.hooksMtx.Unlock()\n\twe.onStartHooks = append(we.onStartHooks, callback)\n}\n\nfunc (we *workflowEngine) OnEnd(callback func(ctx context.Context) error) {\n\twe.hooksMtx.Lock()\n\tdefer we.hooksMtx.Unlock()\n\twe.onEndHooks = append(we.onEndHooks, callback)\n}\n\nfunc (we *workflowEngine) OnError(callback func(ctx context.Context, err error) error) {\n\twe.hooksMtx.Lock()\n\tdefer we.hooksMtx.Unlock()\n\twe.onErrorHooks = append(we.onErrorHooks, callback)\n}\n\nfunc (we *workflowEngine) OnNodeStart(callback func(ctx context.Context, node *Node) error) {\n\twe.hooksMtx.Lock()\n\tdefer we.hooksMtx.Unlock()\n\twe.onNodeStartHooks = append(we.onNodeStartHooks, callback)\n}\n\nfunc (we *workflowEngine) OnNodeEnd(callback func(ctx context.Context, node *Node, res *NodeExecutionResult) error) {\n\twe.hooksMtx.Lock()\n\tdefer we.hooksMtx.Unlock()\n\twe.onNodeEndHooks = append(we.onNodeEndHooks, callback)\n}\n\nfunc (we *workflowEngine) OnNodeError(callback func(ctx context.Context, node *Node, err error) error) {\n\twe.hooksMtx.Lock()\n\tdefer we.hooksMtx.Unlock()\n\twe.onNodeErrorHooks = append(we.onNodeErrorHooks, callback)\n}\n\nfunc (we *workflowEngine) OnNodeLogging(callback func(ctx context.Context, node *Node, log logging.Record) error) {\n\twe.hooksMtx.Lock()\n\tdefer we.hooksMtx.Unlock()\n\twe.onNodeLoggingHooks = append(we.onNodeLoggingHooks, callback)\n}\n\nfunc (we *workflowEngine) executeNode(wfCtx *WorkflowContext, node *Node) error {\n\texecutor, ok := we.executors[node.Type]\n\tif !ok {\n\t\terr := fmt.Errorf(\"workflow engine: no executor registered for node type: '%s'\", node.Type)\n\t\treturn err\n\t} else {\n\t\tlogger := slog.New(logging.NewHookHandler(&logging.HookHandlerOptions{\n\t\t\tLevel: slog.LevelDebug,\n\t\t\tWriteFunc: func(ctx context.Context, record logging.Record) error {\n\t\t\t\twe.fireOnNodeLoggingHooks(ctx, node, record)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}))\n\t\texecutor.SetLogger(logger)\n\t}\n\n\twfCtx.variables.SetScoped(node.Id, stateVarKeyNodeId, node.Id, stateValTypeString)\n\twfCtx.variables.SetScoped(node.Id, stateVarKeyNodeName, node.Data.Name, stateValTypeString)\n\n\t// 节点已禁用，直接跳过执行\n\tif node.Data.Disabled {\n\t\treturn nil\n\t}\n\n\twe.fireOnNodeStartHooks(wfCtx.ctx, node)\n\n\texecCtx := newNodeExecutionContext(wfCtx, node)\n\texecRes, err := executor.Execute(execCtx)\n\tif err != nil && !errors.Is(err, ErrTerminated) {\n\t\tif !errors.Is(err, ErrBlocksException) {\n\t\t\twfCtx.variables.Set(stateVarKeyErrorNodeId, node.Id, stateValTypeString)\n\t\t\twfCtx.variables.Set(stateVarKeyErrorNodeName, node.Data.Name, stateValTypeString)\n\t\t\twfCtx.variables.Set(stateVarKeyErrorMessage, err.Error(), stateValTypeString)\n\t\t}\n\n\t\twe.fireOnNodeErrorHooks(wfCtx.ctx, node, err)\n\t\treturn err\n\t}\n\n\twe.fireOnNodeEndHooks(wfCtx.ctx, node, execRes)\n\n\tif execRes != nil {\n\t\tif execRes.Variables != nil {\n\t\t\tfor _, variable := range execRes.Variables {\n\t\t\t\twfCtx.variables.Add(variable)\n\t\t\t}\n\t\t}\n\n\t\tif execRes.Outputs != nil {\n\t\t\tfor _, output := range execRes.Outputs {\n\t\t\t\twfCtx.inputs.Add(output)\n\t\t\t}\n\t\t}\n\n\t\texecOutputs := lo.Filter(execRes.Outputs, func(state InOutState, _ int) bool { return state.Persistent })\n\t\tif execRes.outputForced || len(execOutputs) > 0 {\n\t\t\toutput := &domain.WorkflowOutput{\n\t\t\t\tWorkflowId: execCtx.WorkflowId,\n\t\t\t\tRunId:      execCtx.RunId,\n\t\t\t\tNodeId:     execCtx.Node.Id,\n\t\t\t\tNodeConfig: execCtx.Node.Data.Config,\n\t\t\t\tSucceeded:  true, // TODO: 目前恒为 true\n\t\t\t}\n\t\t\tif len(execOutputs) > 0 {\n\t\t\t\toutput.Outputs = lo.Map(execOutputs, func(state InOutState, _ int) *domain.WorkflowOutputEntry {\n\t\t\t\t\treturn &domain.WorkflowOutputEntry{\n\t\t\t\t\t\tName:      state.Name,\n\t\t\t\t\t\tType:      state.Type,\n\t\t\t\t\t\tValue:     state.ValueString(),\n\t\t\t\t\t\tValueType: state.ValueType,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t\tif _, err := we.wfoutputRepo.Save(execCtx.Context(), output); err != nil {\n\t\t\t\twe.syslog.Error(\"failed to save node output\", slog.Any(\"error\", err))\n\t\t\t}\n\t\t}\n\n\t\tif execRes.Terminated {\n\t\t\treturn ErrTerminated\n\t\t}\n\t}\n\n\tif err != nil && errors.Is(err, ErrTerminated) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (we *workflowEngine) executeBlocks(wfCtx *WorkflowContext, blocks []*Node) error {\n\terrs := make([]error, 0)\n\n\tfor _, node := range blocks {\n\t\tselect {\n\t\tcase <-wfCtx.ctx.Done():\n\t\t\treturn wfCtx.ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\terr := we.executeNode(wfCtx, node)\n\t\tif err != nil {\n\t\t\t// 如果当前节点是 TryCatch 节点、且在 CatchBlock 分支中没有 End 节点，\n\t\t\t// 则暂存错误，但继续执行下一个节点，直到当前 Blocks 全部执行完毕。\n\t\t\tif node.Type == NodeTypeTryCatch {\n\t\t\t\tif !errors.Is(err, ErrTerminated) {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif len(errs) > 0 {\n\t\tif len(errs) == 1 {\n\t\t\treturn errs[0]\n\t\t}\n\t\treturn errors.Join(errs...)\n\t}\n\n\treturn nil\n}\n\nfunc (we *workflowEngine) fireOnStartHooks(ctx context.Context) {\n\twe.hooksMtx.RLock()\n\tdefer we.hooksMtx.RUnlock()\n\tfor _, cb := range we.onStartHooks {\n\t\tif cbErr := cb(ctx); cbErr != nil {\n\t\t\twe.syslog.Error(\"workflow engine: error in onStart hook\", slog.Any(\"error\", cbErr))\n\t\t}\n\t}\n}\n\nfunc (we *workflowEngine) fireOnEndHooks(ctx context.Context) {\n\twe.hooksMtx.RLock()\n\tdefer we.hooksMtx.RUnlock()\n\tfor _, cb := range we.onEndHooks {\n\t\tif cbErr := cb(ctx); cbErr != nil {\n\t\t\twe.syslog.Error(\"workflow engine: error in onEnd hook\", slog.Any(\"error\", cbErr))\n\t\t}\n\t}\n}\n\nfunc (we *workflowEngine) fireOnErrorHooks(ctx context.Context, err error) {\n\twe.hooksMtx.RLock()\n\tdefer we.hooksMtx.RUnlock()\n\tfor _, cb := range we.onErrorHooks {\n\t\tif cbErr := cb(ctx, err); cbErr != nil {\n\t\t\twe.syslog.Error(\"workflow engine: error in onError hook\", slog.Any(\"error\", cbErr))\n\t\t}\n\t}\n}\n\nfunc (we *workflowEngine) fireOnNodeStartHooks(ctx context.Context, node *Node) {\n\twe.hooksMtx.RLock()\n\tdefer we.hooksMtx.RUnlock()\n\tfor _, cb := range we.onNodeStartHooks {\n\t\tif cbErr := cb(ctx, node); cbErr != nil {\n\t\t\twe.syslog.Error(\"workflow engine: error in onNodeStart hook\", slog.Any(\"error\", cbErr))\n\t\t}\n\t}\n}\n\nfunc (we *workflowEngine) fireOnNodeEndHooks(ctx context.Context, node *Node, result *NodeExecutionResult) {\n\twe.hooksMtx.RLock()\n\tdefer we.hooksMtx.RUnlock()\n\tfor _, cb := range we.onNodeEndHooks {\n\t\tif cbErr := cb(ctx, node, result); cbErr != nil {\n\t\t\twe.syslog.Error(\"workflow engine: error in onNodeEnd hook\", slog.Any(\"error\", cbErr))\n\t\t}\n\t}\n}\n\nfunc (we *workflowEngine) fireOnNodeErrorHooks(ctx context.Context, node *Node, err error) {\n\twe.hooksMtx.RLock()\n\tdefer we.hooksMtx.RUnlock()\n\tfor _, cb := range we.onNodeErrorHooks {\n\t\tif cbErr := cb(ctx, node, err); cbErr != nil {\n\t\t\twe.syslog.Error(\"workflow engine: error in onNodeError hook\", slog.Any(\"error\", cbErr))\n\t\t}\n\t}\n}\n\nfunc (we *workflowEngine) fireOnNodeLoggingHooks(ctx context.Context, node *Node, log logging.Record) {\n\twe.hooksMtx.RLock()\n\tdefer we.hooksMtx.RUnlock()\n\tfor _, cb := range we.onNodeLoggingHooks {\n\t\tif cbErr := cb(ctx, node, log); cbErr != nil {\n\t\t\twe.syslog.Error(\"workflow engine: error in onNodeLogging hook\", slog.Any(\"error\", cbErr))\n\t\t}\n\t}\n}\n\nfunc NewWorkflowEngine() WorkflowEngine {\n\tengine := &workflowEngine{\n\t\texecutors:    make(map[NodeType]NodeExecutor),\n\t\twfoutputRepo: repository.NewWorkflowOutputRepository(),\n\t\tsyslog:       app.GetLogger(),\n\t}\n\tengine.executors[NodeTypeStart] = newStartNodeExecutor()\n\tengine.executors[NodeTypeEnd] = newEndNodeExecutor()\n\tengine.executors[NodeTypeDelay] = newDelayNodeExecutor()\n\tengine.executors[NodeTypeCondition] = newConditionNodeExecutor()\n\tengine.executors[NodeTypeBranchBlock] = newBranchBlockNodeExecutor()\n\tengine.executors[NodeTypeTryCatch] = newTryCatchNodeExecutor()\n\tengine.executors[NodeTypeTryBlock] = newTryBlockNodeExecutor()\n\tengine.executors[NodeTypeCatchBlock] = newCatchBlockNodeExecutor()\n\tengine.executors[NodeTypeBizApply] = newBizApplyNodeExecutor()\n\tengine.executors[NodeTypeBizUpload] = newBizUploadNodeExecutor()\n\tengine.executors[NodeTypeBizMonitor] = newBizMonitorNodeExecutor()\n\tengine.executors[NodeTypeBizDeploy] = newBizDeployNodeExecutor()\n\tengine.executors[NodeTypeBizNotify] = newBizNotifyNodeExecutor()\n\treturn engine\n}\n"
  },
  {
    "path": "internal/workflow/engine/errors.go",
    "content": "package engine\n\nimport (\n\t\"errors\"\n)\n\nvar (\n\t// 表示工作流引擎执行被中断，可能已结束\n\tErrTerminated = errors.New(\"workflow engine: execution was terminated\")\n\t// 表示工作流引擎在执行子节点时发生异常\n\tErrBlocksException = errors.New(\"workflow engine: error occurred when executing blocks\")\n)\n"
  },
  {
    "path": "internal/workflow/engine/executor.go",
    "content": "package engine\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"sync\"\n)\n\ntype NodeExecutor interface {\n\twithLogger\n\n\tExecute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error)\n}\n\ntype nodeExecutor struct {\n\tlogger *slog.Logger\n}\n\nfunc (e *nodeExecutor) SetLogger(logger *slog.Logger) {\n\te.logger = logger\n}\n\ntype NodeExecutionContext struct {\n\tWorkflowContext\n\n\tNode *Node\n}\n\nfunc (c *NodeExecutionContext) SetExecutingWorkflow(workflowId string, runId string, runGraph *Graph) *NodeExecutionContext {\n\tc.WorkflowContext.SetExecutingWorkflow(workflowId, runId, runGraph)\n\treturn c\n}\n\nfunc (c *NodeExecutionContext) SetExecutingNode(node *Node) *NodeExecutionContext {\n\tc.Node = node\n\treturn c\n}\n\nfunc (c *NodeExecutionContext) SetEngine(engine WorkflowEngine) *NodeExecutionContext {\n\tc.WorkflowContext.SetEngine(engine)\n\treturn c\n}\n\nfunc (c *NodeExecutionContext) SetVariablesManager(variables VariableManager) *NodeExecutionContext {\n\tc.WorkflowContext.SetVariablesManager(variables)\n\treturn c\n}\n\nfunc (c *NodeExecutionContext) SetInputsManager(inputs InOutManager) *NodeExecutionContext {\n\tc.WorkflowContext.SetInputsManager(inputs)\n\treturn c\n}\n\nfunc (c *NodeExecutionContext) SetContext(ctx context.Context) *NodeExecutionContext {\n\tc.WorkflowContext.SetContext(ctx)\n\treturn c\n}\n\nfunc newNodeExecutionContext(wfCtx *WorkflowContext, node *Node) *NodeExecutionContext {\n\treturn (&NodeExecutionContext{}).\n\t\tSetExecutingWorkflow(wfCtx.WorkflowId, wfCtx.RunId, wfCtx.RunGraph).\n\t\tSetExecutingNode(node).\n\t\tSetEngine(wfCtx.engine).\n\t\tSetVariablesManager(wfCtx.variables).\n\t\tSetInputsManager(wfCtx.inputs).\n\t\tSetContext(wfCtx.ctx)\n}\n\ntype NodeExecutionResult struct {\n\tnode *Node\n\n\tTerminated bool // 是否终止执行（通常由 End 节点主动触发）\n\n\tvariablesMtx sync.Mutex\n\tVariables    []VariableState\n\n\toutputForced bool // 即使 Outputs 为空，也强制持久化输出\n\toutputsMtx   sync.Mutex\n\tOutputs      []InOutState\n}\n\nfunc (r *NodeExecutionResult) AddVariable(key string, value any, valueType string) {\n\tr.AddVariableWithScope(\"\", key, value, valueType)\n}\n\nfunc (r *NodeExecutionResult) AddVariableWithScope(scope string, key string, value any, valueType string) {\n\tr.addVariableState(VariableState{\n\t\tScope:     scope,\n\t\tKey:       key,\n\t\tValue:     value,\n\t\tValueType: valueType,\n\t})\n}\n\nfunc (r *NodeExecutionResult) addVariableState(state VariableState) {\n\tr.variablesMtx.Lock()\n\tdefer r.variablesMtx.Unlock()\n\n\tif r.Variables == nil {\n\t\tr.Variables = make([]VariableState, 0)\n\t}\n\n\tfor i, item := range r.Variables {\n\t\tif item.Scope == state.Scope && item.Key == state.Key {\n\t\t\tr.Variables[i] = state\n\t\t\treturn\n\t\t}\n\t}\n\tr.Variables = append(r.Variables, state)\n}\n\nfunc (r *NodeExecutionResult) AddOutput(stype string, key string, value any, valueType string) {\n\tr.addOutputState(InOutState{\n\t\tNodeId:     r.node.Id,\n\t\tType:       stype,\n\t\tName:       key,\n\t\tValue:      value,\n\t\tValueType:  valueType,\n\t\tPersistent: false,\n\t})\n}\n\nfunc (r *NodeExecutionResult) AddOutputWithPersistent(stype string, key string, value any, valueType string) {\n\tr.addOutputState(InOutState{\n\t\tNodeId:     r.node.Id,\n\t\tType:       stype,\n\t\tName:       key,\n\t\tValue:      value,\n\t\tValueType:  valueType,\n\t\tPersistent: true,\n\t})\n}\n\nfunc (r *NodeExecutionResult) addOutputState(state InOutState) {\n\tr.outputsMtx.Lock()\n\tdefer r.outputsMtx.Unlock()\n\n\tif r.Outputs == nil {\n\t\tr.Outputs = make([]InOutState, 0)\n\t}\n\n\tfor i, t := range r.Outputs {\n\t\tif t.NodeId == state.NodeId && t.Name == state.Name {\n\t\t\tr.Outputs[i] = state\n\t\t\treturn\n\t\t}\n\t}\n\tr.Outputs = append(r.Outputs, state)\n}\n\nfunc newNodeExecutionResult(node *Node) *NodeExecutionResult {\n\treturn &NodeExecutionResult{\n\t\tnode:      node,\n\t\tVariables: make([]VariableState, 0),\n\t\tOutputs:   make([]InOutState, 0),\n\t}\n}\n"
  },
  {
    "path": "internal/workflow/engine/executor_bizapply.go",
    "content": "package engine\n\nimport (\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"maps\"\n\t\"math\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\tlegocertifier \"github.com/go-acme/lego/v4/certificate\"\n\t\"github.com/go-acme/lego/v4/lego\"\n\tlegolog \"github.com/go-acme/lego/v4/log\"\n\t\"github.com/samber/lo\"\n\t\"github.com/xhit/go-str2duration/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/certacme\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/internal/repository\"\n\t\"github.com/certimate-go/certimate/internal/tools/mproc\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcertkey \"github.com/certimate-go/certimate/pkg/utils/cert/key\"\n\txenv \"github.com/certimate-go/certimate/pkg/utils/env\"\n)\n\nvar envMultiProc = true\n\nfunc init() {\n\tenvMultiProc = xenv.GetOrDefaultBool(\"CERTIMATE_WORKFLOW_MULTIPROC\", true)\n}\n\nconst (\n\tBizApplyKeySourceAuto   = \"auto\"\n\tBizApplyKeySourceReuse  = \"reuse\"\n\tBizApplyKeySourceCustom = \"custom\"\n)\n\n/**\n * Outputs:\n *   - ref: \"certificate\": string\n *\n * Variables:\n *   - \"node.skipped\": boolean\n *   - \"certificate.commanName\": string\n *   - \"certificate.subjectAltNames\": string\n *   - \"certificate.notBefore\": datetime\n *   - \"certificate.notAfter\": datetime\n *   - \"certificate.hoursLeft\": number\n *   - \"certificate.daysLeft\": number\n *   - \"certificate.validity\": boolean\n */\ntype bizApplyNodeExecutor struct {\n\tnodeExecutor\n\n\taccessRepo      accessRepository\n\tcertificateRepo certificateRepository\n\twfoutputRepo    workflowOutputRepository\n}\n\nfunc (ne *bizApplyNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) {\n\texecRes := newNodeExecutionResult(execCtx.Node)\n\n\tnodeCfg := execCtx.Node.Data.Config.AsBizApply()\n\tne.logger.Info(\"ready to request certificate ...\", slog.Any(\"config\", nodeCfg))\n\n\t// 查询上次执行结果\n\tlastOutput, lastCertificate, err := ne.getLastOutputArtifacts(execCtx)\n\tif err != nil {\n\t\treturn execRes, err\n\t} else {\n\t\tif lastOutput != nil {\n\t\t\tne.logger.Info(fmt.Sprintf(\"found last node output #%s record\", lastOutput.RunId))\n\t\t}\n\n\t\tif lastCertificate != nil {\n\t\t\tne.setOuputsOfResult(execCtx, execRes, lastCertificate, false)\n\t\t\tne.setVariablesOfResult(execCtx, execRes, lastCertificate)\n\t\t\tne.logger.Info(fmt.Sprintf(\"found last certificate #%s record\", lastCertificate.Id))\n\t\t}\n\t}\n\n\t// 检测是否可以跳过本次执行\n\tif skippable, reason := ne.checkCanSkip(execCtx, lastOutput, lastCertificate); skippable {\n\t\tne.logger.Info(fmt.Sprintf(\"skip this application, because %s\", reason))\n\n\t\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, true, stateValTypeBoolean)\n\t\treturn execRes, nil\n\t} else {\n\t\tif reason != \"\" {\n\t\t\tne.logger.Info(fmt.Sprintf(\"re-apply, because %s\", reason))\n\t\t} else {\n\t\t\tne.logger.Info(\"no found last requested certificate, begin to apply\")\n\t\t}\n\n\t\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, false, stateValTypeBoolean)\n\t}\n\n\t// 申请证书\n\tobtainResp, err := ne.executeObtain(execCtx, &nodeCfg, lastCertificate)\n\tif err != nil {\n\t\treturn execRes, err\n\t}\n\n\t// 保存证书实体\n\tcertificate := &domain.Certificate{\n\t\tSource:            domain.CertificateSourceTypeRequest,\n\t\tCertificate:       obtainResp.FullChainCertificate,\n\t\tPrivateKey:        obtainResp.PrivateKey,\n\t\tIssuerCertificate: obtainResp.IssuerCertificate,\n\t\tACMEAcctUrl:       obtainResp.ACMEAcctUrl,\n\t\tACMECertUrl:       obtainResp.ACMECertUrl,\n\t\tWorkflowId:        execCtx.WorkflowId,\n\t\tWorkflowRunId:     execCtx.RunId,\n\t\tWorkflowNodeId:    execCtx.Node.Id,\n\t}\n\tcertificate.PopulateFromPEM(obtainResp.FullChainCertificate, obtainResp.PrivateKey)\n\tif certificate, err := ne.certificateRepo.Save(execCtx.Context(), certificate); err != nil {\n\t\tne.logger.Warn(\"could not save certificate\")\n\t\treturn execRes, err\n\t} else {\n\t\tne.logger.Info(\"certificate saved\", slog.String(\"recordId\", certificate.Id))\n\t}\n\n\t// 保存 ARI 替换状态\n\tif lastCertificate != nil && obtainResp.ARIReplaced {\n\t\tlastCertificate.IsRenewed = true\n\t\tne.certificateRepo.Save(execCtx.Context(), lastCertificate)\n\t}\n\n\t// 节点输出\n\tne.setOuputsOfResult(execCtx, execRes, certificate, true)\n\tne.setVariablesOfResult(execCtx, execRes, certificate)\n\n\tne.logger.Info(\"application completed\")\n\treturn execRes, nil\n}\n\nfunc (ne *bizApplyNodeExecutor) getLastOutputArtifacts(execCtx *NodeExecutionContext) (*domain.WorkflowOutput, *domain.Certificate, error) {\n\tlastOutput, err := ne.wfoutputRepo.GetByWorkflowIdAndNodeId(execCtx.Context(), execCtx.WorkflowId, execCtx.Node.Id)\n\tif err != nil && !domain.IsRecordNotFoundError(err) {\n\t\treturn nil, nil, fmt.Errorf(\"failed to get last output record of node #%s: %w\", execCtx.Node.Id, err)\n\t}\n\n\tif lastOutput != nil {\n\t\tlastCertificate, err := ne.certificateRepo.GetByWorkflowRunIdAndNodeId(execCtx.Context(), lastOutput.RunId, lastOutput.NodeId)\n\t\tif err != nil && !domain.IsRecordNotFoundError(err) {\n\t\t\treturn lastOutput, nil, fmt.Errorf(\"failed to get last certificate record of node #%s: %w\", execCtx.Node.Id, err)\n\t\t}\n\n\t\treturn lastOutput, lastCertificate, nil\n\t}\n\n\treturn lastOutput, nil, nil\n}\n\nfunc (ne *bizApplyNodeExecutor) checkCanSkip(execCtx *NodeExecutionContext, lastOutput *domain.WorkflowOutput, lastCertificate *domain.Certificate) (_skip bool, _reason string) {\n\tthisNodeCfg := execCtx.Node.Data.Config.AsBizApply()\n\n\tif lastOutput != nil && lastOutput.Succeeded {\n\t\t// 比较和上次申请时的关键配置（即影响证书签发的）参数是否一致\n\t\tlastNodeCfg := lastOutput.NodeConfig.AsBizApply()\n\n\t\tif !slices.Equal(thisNodeCfg.Domains, lastNodeCfg.Domains) {\n\t\t\treturn false, \"the configuration item 'Domains' changed\"\n\t\t}\n\t\tif !slices.Equal(thisNodeCfg.IPAddrs, lastNodeCfg.IPAddrs) {\n\t\t\treturn false, \"the configuration item 'IPAddrs' changed\"\n\t\t}\n\t\tif thisNodeCfg.ContactEmail != lastNodeCfg.ContactEmail {\n\t\t\treturn false, \"the configuration item 'ContactEmail' changed\"\n\t\t}\n\t\tif thisNodeCfg.Provider != lastNodeCfg.Provider {\n\t\t\treturn false, \"the configuration item 'Provider' changed\"\n\t\t}\n\t\tif thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId {\n\t\t\treturn false, \"the configuration item 'ProviderAccessId' changed\"\n\t\t}\n\t\tif !maps.Equal(thisNodeCfg.ProviderConfig, lastNodeCfg.ProviderConfig) {\n\t\t\treturn false, \"the configuration item 'ProviderConfig' changed\"\n\t\t}\n\t\tif thisNodeCfg.CAProvider != lastNodeCfg.CAProvider {\n\t\t\treturn false, \"the configuration item 'CAProvider' changed\"\n\t\t}\n\t\tif thisNodeCfg.CAProviderAccessId != lastNodeCfg.CAProviderAccessId {\n\t\t\treturn false, \"the configuration item 'CAProviderAccessId' changed\"\n\t\t}\n\t\tif !maps.Equal(thisNodeCfg.CAProviderConfig, lastNodeCfg.CAProviderConfig) {\n\t\t\treturn false, \"the configuration item 'CAProviderConfig' changed\"\n\t\t}\n\t\tif thisNodeCfg.KeyAlgorithm != lastNodeCfg.KeyAlgorithm {\n\t\t\treturn false, \"the configuration item 'KeyAlgorithm' changed\"\n\t\t}\n\t\tif thisNodeCfg.KeySource == BizApplyKeySourceCustom && thisNodeCfg.KeyContent != lastNodeCfg.KeyContent {\n\t\t\treturn false, \"the configuration item 'KeyContent' changed\"\n\t\t}\n\t\tif thisNodeCfg.ValidityLifetime != lastNodeCfg.ValidityLifetime {\n\t\t\treturn false, \"the configuration item 'ValidityLifetime' changed\"\n\t\t}\n\t\tif thisNodeCfg.PreferredChain != lastNodeCfg.PreferredChain {\n\t\t\treturn false, \"the configuration item 'PreferredChain' changed\"\n\t\t}\n\t\tif thisNodeCfg.ACMEProfile != lastNodeCfg.ACMEProfile {\n\t\t\treturn false, \"the configuration item 'ACMEProfile' changed\"\n\t\t}\n\t\tif thisNodeCfg.DisableCommonName != lastNodeCfg.DisableCommonName {\n\t\t\treturn false, \"the configuration item 'DisableCommonName' changed\"\n\t\t}\n\t}\n\n\tif lastCertificate != nil {\n\t\tif lastCertificate.IsRevoked {\n\t\t\treturn false, \"the last requested certificate has been revoked\"\n\t\t}\n\n\t\trenewalInterval := time.Duration(thisNodeCfg.SkipBeforeExpiryDays) * time.Hour * 24\n\t\texpirationTime := time.Until(lastCertificate.ValidityNotAfter)\n\t\tdaysLeft := int(math.Floor(expirationTime.Hours() / 24))\n\t\tif expirationTime > renewalInterval {\n\t\t\treturn true, fmt.Sprintf(\"the last requested certificate expires in %d day(s), next renewal will be in %d day(s)\", daysLeft, thisNodeCfg.SkipBeforeExpiryDays)\n\t\t}\n\n\t\treturn false, fmt.Sprintf(\"the last requested certificate expires in %d day(s), the renewal window period has been reached\", daysLeft)\n\t}\n\n\treturn false, \"\"\n}\n\nfunc (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nodeCfg *domain.WorkflowNodeConfigForBizApply, lastCertificate *domain.Certificate) (*certacme.ObtainCertificateResponse, error) {\n\t// 读取私钥算法\n\t// 如果复用私钥，则保持算法一致\n\tlegoKeyType, err := domain.CertificateKeyAlgorithmType(nodeCfg.KeyAlgorithm).KeyType()\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tswitch nodeCfg.KeySource {\n\t\tcase BizApplyKeySourceAuto:\n\t\t\tbreak\n\t\tcase BizApplyKeySourceReuse:\n\t\t\tif lastCertificate != nil {\n\t\t\t\tlegoKeyType, _ = lastCertificate.KeyAlgorithm.KeyType()\n\t\t\t}\n\t\tcase BizApplyKeySourceCustom:\n\t\t\tprivkey, err := xcert.ParsePrivateKeyFromPEM(nodeCfg.KeyContent)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"could not parse custom private key: %w\", err)\n\t\t\t} else {\n\t\t\t\tprivkeyAlg, privkeySize, _ := xcertkey.GetPrivateKeyAlgorithm(privkey)\n\t\t\t\tswitch privkeyAlg {\n\t\t\t\tcase x509.RSA:\n\t\t\t\t\tif nodeCfg.KeyAlgorithm != fmt.Sprintf(\"RSA%d\", privkeySize) {\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"could not parse custom private key: unsupported algorithm or key size\")\n\t\t\t\t\t}\n\t\t\t\tcase x509.ECDSA:\n\t\t\t\t\tif nodeCfg.KeyAlgorithm != fmt.Sprintf(\"EC%d\", privkeySize) {\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"could not parse custom private key: unsupported algorithm or key size\")\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\treturn nil, fmt.Errorf(\"could not parse custom private key: unsupported algorithm\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// 读取质询提供商授权\n\tproviderAccessConfig := make(map[string]any)\n\tif nodeCfg.ProviderAccessId != \"\" {\n\t\tif access, err := ne.accessRepo.GetById(execCtx.Context(), nodeCfg.ProviderAccessId); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get access #%s record: %w\", nodeCfg.ProviderAccessId, err)\n\t\t} else {\n\t\t\tproviderAccessConfig = access.Config\n\t\t}\n\t}\n\n\t// 读取证书颁发机构授权\n\tcaAccessConfig := make(map[string]any)\n\tif nodeCfg.CAProviderAccessId != \"\" {\n\t\tif access, err := ne.accessRepo.GetById(execCtx.Context(), nodeCfg.CAProviderAccessId); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get access #%s record: %w\", nodeCfg.CAProviderAccessId, err)\n\t\t} else {\n\t\t\tcaAccessConfig = access.Config\n\t\t}\n\t}\n\n\t// 初始化 ACME 配置项\n\tlegoOptions := &certacme.ACMEConfigOptions{\n\t\tCAProvider:       nodeCfg.CAProvider,\n\t\tCAAccessConfig:   caAccessConfig,\n\t\tCAProviderConfig: nodeCfg.CAProviderConfig,\n\t\tCertifierKeyType: legoKeyType,\n\t}\n\tlegoConfig, err := certacme.NewACMEConfig(legoOptions)\n\tif err != nil {\n\t\tne.logger.Warn(\"could not initialize acme config\")\n\t\treturn nil, err\n\t} else {\n\t\tne.logger.Info(\"acme config initialized\", slog.String(\"acmeDirUrl\", legoConfig.CADirUrl))\n\t}\n\n\t// 初始化 ACME 账户\n\t// 注意此步骤仍需在主进程中进行，以保证并发安全\n\tlegoUser, err := certacme.NewACMEAccountWithSingleFlight(legoConfig, nodeCfg.ContactEmail)\n\tif err != nil {\n\t\tne.logger.Warn(\"could not initialize acme account\")\n\t\treturn nil, err\n\t} else {\n\t\tne.logger.Info(\"acme account initialized\", slog.String(\"acmeAcctUrl\", legoUser.ACMEAcctUrl))\n\t}\n\n\t// 构造证书申请请求\n\tlegoDomains := make([]string, 0)\n\tlegoDomains = append(legoDomains, nodeCfg.Domains...)\n\tlegoDomains = append(legoDomains, nodeCfg.IPAddrs...)\n\tobtainReq := &certacme.ObtainCertificateRequest{\n\t\tDomainOrIPs:    legoDomains,\n\t\tPrivateKeyType: legoKeyType,\n\t\tPrivateKeyPEM: lo.\n\t\t\tIf(nodeCfg.KeySource == BizApplyKeySourceAuto, \"\").\n\t\t\tElseF(func() string {\n\t\t\t\tswitch nodeCfg.KeySource {\n\t\t\t\tcase BizApplyKeySourceReuse:\n\t\t\t\t\tif lastCertificate != nil {\n\t\t\t\t\t\treturn lastCertificate.PrivateKey\n\t\t\t\t\t}\n\t\t\t\tcase BizApplyKeySourceCustom:\n\t\t\t\t\treturn nodeCfg.KeyContent\n\t\t\t\t}\n\t\t\t\treturn \"\"\n\t\t\t}),\n\t\tValidityNotAfter: lo.\n\t\t\tIf(nodeCfg.ValidityLifetime == \"\", time.Time{}).\n\t\t\tElseF(func() time.Time {\n\t\t\t\tduration, err := str2duration.ParseDuration(nodeCfg.ValidityLifetime)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn time.Time{}\n\t\t\t\t}\n\t\t\t\treturn time.Now().Add(duration)\n\t\t\t}),\n\t\tNoCommonName:           nodeCfg.DisableCommonName,\n\t\tChallengeType:          nodeCfg.ChallengeType,\n\t\tProvider:               nodeCfg.Provider,\n\t\tProviderAccessConfig:   providerAccessConfig,\n\t\tProviderExtendedConfig: nodeCfg.ProviderConfig,\n\t\tDisableFollowCNAME:     nodeCfg.DisableFollowCNAME,\n\t\tNameservers:            nodeCfg.Nameservers,\n\t\tDnsPropagationWait:     nodeCfg.DnsPropagationWait,\n\t\tDnsPropagationTimeout:  nodeCfg.DnsPropagationTimeout,\n\t\tDnsTTL:                 nodeCfg.DnsTTL,\n\t\tHttpDelayWait:          nodeCfg.HttpDelayWait,\n\t\tPreferredChain:         nodeCfg.PreferredChain,\n\t\tACMEProfile:            nodeCfg.ACMEProfile,\n\t\tARIReplacesAcctUrl: lo.\n\t\t\tIf(nodeCfg.DisableARI || lastCertificate == nil, \"\").\n\t\t\tElseF(func() string {\n\t\t\t\tif lastCertificate.IsRenewed {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\t\t\t\treturn lastCertificate.ACMEAcctUrl\n\t\t\t}),\n\t\tARIReplacesCertId: lo.\n\t\t\tIf(nodeCfg.DisableARI || lastCertificate == nil, \"\").\n\t\t\tElseF(func() string {\n\t\t\t\tif lastCertificate.IsRenewed {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\n\t\t\t\tnewCertSan := slices.Clone(nodeCfg.Domains)\n\t\t\t\toldCertSan := strings.Split(lastCertificate.SubjectAltNames, \";\")\n\t\t\t\tslices.Sort(newCertSan)\n\t\t\t\tslices.Sort(oldCertSan)\n\t\t\t\tif !slices.Equal(newCertSan, oldCertSan) {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\n\t\t\t\toldCertX509, err := xcert.ParseCertificateFromPEM(lastCertificate.Certificate)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\n\t\t\t\toldARICertId, _ := legocertifier.MakeARICertID(oldCertX509)\n\t\t\t\treturn oldARICertId\n\t\t\t}),\n\t}\n\n\t// 如果启用多进程模式，发送指令\n\tif envMultiProc {\n\t\ttype InData struct {\n\t\t\tAccount *certacme.ACMEAccount              `json:\"account,omitempty\"`\n\t\t\tRequest *certacme.ObtainCertificateRequest `json:\"request,omitempty\"`\n\t\t}\n\n\t\ttype OutData struct {\n\t\t\tResponse *certacme.ObtainCertificateResponse `json:\"response\"`\n\t\t}\n\n\t\tmsender := mproc.NewSender[InData, OutData](\"certapply\", ne.logger)\n\t\tmoutput, err := msender.SendWithContext(execCtx.Context(), &InData{\n\t\t\tAccount: legoUser,\n\t\t\tRequest: obtainReq,\n\t\t})\n\t\tif err != nil {\n\t\t\tne.logger.Warn(\"could not obtain certificate\")\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif moutput.Response != nil {\n\t\t\treturn moutput.Response, nil\n\t\t} else {\n\t\t\tpanic(\"unreachable\")\n\t\t}\n\t}\n\n\t// 初始化 ACME 客户端\n\tlegolog.Logger = certacme.NewLegoLogger(app.GetLogger())\n\tlegoClient, err := certacme.NewACMEClientWithAccount(legoUser, func(c *lego.Config) error {\n\t\tc.UserAgent = app.AppUserAgent\n\t\tc.Certificate.KeyType = legoKeyType\n\t\tc.Certificate.DisableCommonName = obtainReq.NoCommonName\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tne.logger.Warn(\"could not initialize acme client\")\n\t\treturn nil, err\n\t}\n\n\t// 执行申请证书请求\n\tobtainResp, err := legoClient.ObtainCertificate(execCtx.Context(), obtainReq)\n\tif err != nil {\n\t\tne.logger.Warn(\"could not obtain certificate\")\n\t\treturn nil, err\n\t}\n\n\treturn obtainResp, nil\n}\n\nfunc (ne *bizApplyNodeExecutor) setOuputsOfResult(execCtx *NodeExecutionContext, execRes *NodeExecutionResult, certificate *domain.Certificate, persistent bool) {\n\tif certificate != nil {\n\t\tkey := \"certificate\"\n\t\tvalue := fmt.Sprintf(\"%s#%s\", domain.CollectionNameCertificate, certificate.Id)\n\t\tif persistent {\n\t\t\texecRes.AddOutputWithPersistent(stateIOTypeRef, key, value, stateValTypeString)\n\t\t} else {\n\t\t\texecRes.AddOutput(stateIOTypeRef, key, value, stateValTypeString)\n\t\t}\n\t}\n}\n\nfunc (ne *bizApplyNodeExecutor) setVariablesOfResult(execCtx *NodeExecutionContext, execRes *NodeExecutionResult, certificate *domain.Certificate) {\n\tvar vCommonName string\n\tvar vSubjectAltNames string\n\tvar vNotBefore time.Time\n\tvar vNotAfter time.Time\n\tvar vHoursLeft int32\n\tvar vDaysLeft int32\n\tvar vValidity bool\n\n\tif certificate != nil {\n\t\tvCommonName = strings.Split(certificate.SubjectAltNames, \";\")[0]\n\t\tvSubjectAltNames = certificate.SubjectAltNames\n\t\tvNotBefore = certificate.ValidityNotBefore\n\t\tvNotAfter = certificate.ValidityNotAfter\n\t\tvHoursLeft = int32(math.Floor(time.Until(certificate.ValidityNotAfter).Hours()))\n\t\tvDaysLeft = int32(math.Floor(time.Until(certificate.ValidityNotAfter).Hours() / 24))\n\t\tvValidity = certificate.ValidityNotAfter.After(time.Now())\n\t}\n\n\texecRes.AddVariable(stateVarKeyCertificateDomain, vCommonName, stateValTypeString)\n\texecRes.AddVariable(stateVarKeyCertificateDomains, vSubjectAltNames, stateValTypeString)\n\texecRes.AddVariable(stateVarKeyCertificateCommonName, vCommonName, stateValTypeString)\n\texecRes.AddVariable(stateVarKeyCertificateSubjectAltNames, vSubjectAltNames, stateValTypeString)\n\texecRes.AddVariable(stateVarKeyCertificateNotBefore, vNotBefore, stateValTypeDateTime)\n\texecRes.AddVariable(stateVarKeyCertificateNotAfter, vNotAfter, stateValTypeDateTime)\n\texecRes.AddVariable(stateVarKeyCertificateHoursLeft, vHoursLeft, stateValTypeNumber)\n\texecRes.AddVariable(stateVarKeyCertificateDaysLeft, vDaysLeft, stateValTypeNumber)\n\texecRes.AddVariable(stateVarKeyCertificateValidity, vValidity, stateValTypeBoolean)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDomain, vCommonName, stateValTypeString)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDomains, vSubjectAltNames, stateValTypeString)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateCommonName, vCommonName, stateValTypeString)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateSubjectAltNames, vSubjectAltNames, stateValTypeString)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateNotBefore, vNotBefore, stateValTypeDateTime)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateNotAfter, vNotAfter, stateValTypeDateTime)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateHoursLeft, vHoursLeft, stateValTypeNumber)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDaysLeft, vDaysLeft, stateValTypeNumber)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, vValidity, stateValTypeBoolean)\n}\n\nfunc newBizApplyNodeExecutor() NodeExecutor {\n\treturn &bizApplyNodeExecutor{\n\t\tnodeExecutor:    nodeExecutor{logger: slog.Default()},\n\t\taccessRepo:      repository.NewAccessRepository(),\n\t\tcertificateRepo: repository.NewCertificateRepository(),\n\t\twfoutputRepo:    repository.NewWorkflowOutputRepository(),\n\t}\n}\n"
  },
  {
    "path": "internal/workflow/engine/executor_bizdeploy.go",
    "content": "package engine\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"maps\"\n\t\"strings\"\n\n\t\"github.com/certimate-go/certimate/internal/certmgmt\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/internal/repository\"\n)\n\n/**\n * Inputs:\n *   - ref: \"certificate\": string\n *\n * Variables:\n *   - \"node.skipped\": boolean\n */\ntype bizDeployNodeExecutor struct {\n\tnodeExecutor\n\n\taccessRepo      accessRepository\n\tcertificateRepo certificateRepository\n\twfoutputRepo    workflowOutputRepository\n}\n\nfunc (ne *bizDeployNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) {\n\texecRes := newNodeExecutionResult(execCtx.Node)\n\n\tnodeCfg := execCtx.Node.Data.Config.AsBizDeploy()\n\tne.logger.Info(\"ready to deploy certificate ...\", slog.Any(\"config\", nodeCfg))\n\n\t// 查询上次执行结果\n\tlastOutput, err := ne.getLastOutputArtifacts(execCtx)\n\tif err != nil {\n\t\treturn execRes, err\n\t} else {\n\t\tif lastOutput != nil {\n\t\t\tne.logger.Info(fmt.Sprintf(\"found last node output #%s record\", lastOutput.RunId))\n\t\t}\n\t}\n\n\t// 获取前序节点输出证书\n\tvar inputCertificate *domain.Certificate\n\tif inputState, ok := execCtx.inputs.Get(nodeCfg.CertificateOutputNodeId, \"certificate\"); ok {\n\t\tif inputStateValue, ok := inputState.Value.(string); ok {\n\t\t\ts := strings.Split(inputStateValue, \"#\")\n\t\t\tif len(s) == 2 {\n\t\t\t\tcertificate, err := ne.certificateRepo.GetById(execCtx.Context(), s[1])\n\t\t\t\tif err != nil {\n\t\t\t\t\tne.logger.Warn(\"could not get input certificate\")\n\t\t\t\t\treturn execRes, err\n\t\t\t\t}\n\n\t\t\t\tinputCertificate = certificate\n\t\t\t}\n\t\t}\n\t}\n\tif inputCertificate == nil {\n\t\treturn execRes, fmt.Errorf(\"invalid input certificate\")\n\t}\n\n\t// 检测是否可以跳过本次执行\n\tif lastOutput != nil && inputCertificate.CreatedAt.Before(lastOutput.UpdatedAt) {\n\t\tif skippable, reason := ne.checkCanSkip(execCtx, lastOutput); skippable {\n\t\t\tne.logger.Info(fmt.Sprintf(\"skip this deployment, because %s\", reason))\n\n\t\t\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, true, stateValTypeBoolean)\n\t\t\treturn execRes, nil\n\t\t} else if reason != \"\" {\n\t\t\tne.logger.Info(fmt.Sprintf(\"re-deploy, because %s\", reason))\n\n\t\t\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, false, stateValTypeBoolean)\n\t\t}\n\t} else {\n\t\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, false, stateValTypeBoolean)\n\t}\n\n\t// 读取部署提供商授权\n\tproviderAccessConfig := make(map[string]any)\n\tif nodeCfg.ProviderAccessId != \"\" {\n\t\tif access, err := ne.accessRepo.GetById(execCtx.Context(), nodeCfg.ProviderAccessId); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get access #%s record: %w\", nodeCfg.ProviderAccessId, err)\n\t\t} else {\n\t\t\tproviderAccessConfig = access.Config\n\t\t}\n\t}\n\n\t// 部署证书\n\tdeployer := certmgmt.NewClient(certmgmt.WithLogger(ne.logger))\n\tdeployReq := &certmgmt.DeployCertificateRequest{\n\t\tProvider:               nodeCfg.Provider,\n\t\tProviderAccessConfig:   providerAccessConfig,\n\t\tProviderExtendedConfig: nodeCfg.ProviderConfig,\n\t\tCertificate:            inputCertificate.Certificate,\n\t\tPrivateKey:             inputCertificate.PrivateKey,\n\t}\n\tif _, err := deployer.DeployCertificate(execCtx.Context(), deployReq); err != nil {\n\t\tne.logger.Warn(\"could not deploy certificate\")\n\t\treturn execRes, err\n\t}\n\n\t// 节点输出\n\texecRes.outputForced = true\n\n\tne.logger.Info(\"deployment completed\")\n\treturn execRes, nil\n}\n\nfunc (ne *bizDeployNodeExecutor) getLastOutputArtifacts(execCtx *NodeExecutionContext) (*domain.WorkflowOutput, error) {\n\tlastOutput, err := ne.wfoutputRepo.GetByWorkflowIdAndNodeId(execCtx.Context(), execCtx.WorkflowId, execCtx.Node.Id)\n\tif err != nil && !domain.IsRecordNotFoundError(err) {\n\t\treturn nil, fmt.Errorf(\"failed to get last output record of node #%s: %w\", execCtx.Node.Id, err)\n\t}\n\n\treturn lastOutput, nil\n}\n\nfunc (ne *bizDeployNodeExecutor) checkCanSkip(execCtx *NodeExecutionContext, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) {\n\tthisNodeCfg := execCtx.Node.Data.Config.AsBizDeploy()\n\n\tif lastOutput != nil && lastOutput.Succeeded {\n\t\t// 比较和上次部署时的关键配置（即影响证书部署的）参数是否一致\n\t\tlastNodeCfg := lastOutput.NodeConfig.AsBizDeploy()\n\n\t\tif thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId {\n\t\t\treturn false, \"the configuration item 'ProviderAccessId' changed\"\n\t\t}\n\t\tif !maps.Equal(thisNodeCfg.ProviderConfig, lastNodeCfg.ProviderConfig) {\n\t\t\treturn false, \"the configuration item 'ProviderConfig' changed\"\n\t\t}\n\n\t\tif thisNodeCfg.SkipOnLastSucceeded {\n\t\t\treturn true, \"the last deployment already completed\"\n\t\t}\n\t}\n\n\treturn false, \"\"\n}\n\nfunc newBizDeployNodeExecutor() NodeExecutor {\n\treturn &bizDeployNodeExecutor{\n\t\tnodeExecutor:    nodeExecutor{logger: slog.Default()},\n\t\taccessRepo:      repository.NewAccessRepository(),\n\t\tcertificateRepo: repository.NewCertificateRepository(),\n\t\twfoutputRepo:    repository.NewWorkflowOutputRepository(),\n\t}\n}\n"
  },
  {
    "path": "internal/workflow/engine/executor_bizmonitor.go",
    "content": "package engine\n\nimport (\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"math\"\n\t\"net\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/repository\"\n\txcertx509 \"github.com/certimate-go/certimate/pkg/utils/cert/x509\"\n\txhttp \"github.com/certimate-go/certimate/pkg/utils/http\"\n\txtls \"github.com/certimate-go/certimate/pkg/utils/tls\"\n)\n\n/**\n * Variables:\n *   - \"certificate.commanName\": string\n *   - \"certificate.subjectAltNames\": string\n *   - \"certificate.notBefore\": datetime\n *   - \"certificate.notAfter\": datetime\n *   - \"certificate.hoursLeft\": number\n *   - \"certificate.daysLeft\": number\n *   - \"certificate.validity\": boolean\n */\ntype bizMonitorNodeExecutor struct {\n\tnodeExecutor\n\n\tcertificateRepo certificateRepository\n}\n\nfunc (ne *bizMonitorNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) {\n\texecRes := newNodeExecutionResult(execCtx.Node)\n\n\tnodeCfg := execCtx.Node.Data.Config.AsBizMonitor()\n\tne.logger.Info(\"ready to monitor certificate ...\", slog.Any(\"config\", nodeCfg))\n\n\ttargetAddr := net.JoinHostPort(nodeCfg.Host, strconv.Itoa(int(nodeCfg.Port)))\n\tif nodeCfg.Port == 0 {\n\t\ttargetAddr = net.JoinHostPort(nodeCfg.Host, \"443\")\n\t}\n\n\ttargetDomain := nodeCfg.Domain\n\tif targetDomain == \"\" {\n\t\ttargetDomain = nodeCfg.Host\n\t}\n\n\tne.logger.Info(fmt.Sprintf(\"retrieving certificate at %s (domain: %s)\", targetAddr, targetDomain))\n\n\tconst MAX_ATTEMPTS = 3\n\tconst RETRY_INTERVAL = 2 * time.Second\n\tvar err error\n\tvar certs []*x509.Certificate\n\tfor attempt := 0; attempt < MAX_ATTEMPTS; attempt++ {\n\t\tif attempt > 0 {\n\t\t\tne.logger.Info(fmt.Sprintf(\"retry %d time(s) ...\", attempt))\n\n\t\t\tctx := execCtx.Context()\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn execRes, ctx.Err()\n\t\t\tcase <-time.After(RETRY_INTERVAL):\n\t\t\t}\n\t\t}\n\n\t\tcerts, err = ne.tryRetrievePeerCertificates(execCtx, targetAddr, targetDomain, nodeCfg.RequestPath)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif err != nil {\n\t\tne.logger.Warn(\"could not retrieve certificate\")\n\t\treturn execRes, err\n\t} else {\n\t\tif len(certs) == 0 {\n\t\t\tne.logger.Warn(\"no ssl certificates retrieved in http response\")\n\n\t\t\tne.setVariablesOfResult(execCtx, execRes, nil)\n\t\t} else {\n\t\t\tcert := certs[0] // 只取证书链中的第一个证书，即服务器证书\n\t\t\tne.logger.Info(fmt.Sprintf(\"ssl certificate retrieved (serial='%s', subject='%s', issuer='%s', not_before='%s', not_after='%s', sans='%s')\",\n\t\t\t\tcert.SerialNumber, cert.Subject.String(), cert.Issuer.String(),\n\t\t\t\tcert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339),\n\t\t\t\tstrings.Join(xcertx509.GetSubjectAltNames(cert), \";\")),\n\t\t\t)\n\t\t\tne.setVariablesOfResult(execCtx, execRes, cert)\n\n\t\t\tnow := time.Now()\n\t\t\tisCertPeriodValid := now.Before(cert.NotAfter) && now.After(cert.NotBefore)\n\t\t\tisCertHostMatched := cert.VerifyHostname(targetDomain) == nil\n\t\t\tdaysLeft := int32(math.Floor(time.Until(cert.NotAfter).Hours() / 24))\n\t\t\tvalidated := isCertPeriodValid && isCertHostMatched\n\n\t\t\tif validated {\n\t\t\t\tne.logger.Info(fmt.Sprintf(\"the certificate is valid, and will expire in %d day(s)\", daysLeft))\n\t\t\t} else {\n\t\t\t\tif !isCertHostMatched {\n\t\t\t\t\tne.logger.Warn(\"the certificate is invalid, because it is not matched the host\")\n\t\t\t\t} else if !isCertPeriodValid {\n\t\t\t\t\tne.logger.Warn(\"the certificate is invalid, because it is either expired or not yet valid\")\n\t\t\t\t} else {\n\t\t\t\t\tne.logger.Warn(\"the certificate is invalid\")\n\t\t\t\t}\n\n\t\t\t\t// 除了验证证书有效期，还要确保证书与域名匹配\n\t\t\t\texecRes.AddVariable(stateVarKeyCertificateValidity, false, stateValTypeBoolean)\n\t\t\t\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, false, stateValTypeBoolean)\n\t\t\t}\n\t\t}\n\t}\n\n\tne.logger.Info(\"monitoring completed\")\n\treturn execRes, nil\n}\n\nfunc (ne *bizMonitorNodeExecutor) tryRetrievePeerCertificates(execCtx *NodeExecutionContext, addr, domain, requestPath string) ([]*x509.Certificate, error) {\n\ttransport := xhttp.NewDefaultTransport()\n\ttransport.DisableKeepAlives = true\n\ttransport.TLSClientConfig = xtls.NewInsecureConfig()\n\n\tclient := &http.Client{\n\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {\n\t\t\treturn http.ErrUseLastResponse\n\t\t},\n\t\tTimeout:   30 * time.Second,\n\t\tTransport: transport,\n\t}\n\n\turl := fmt.Sprintf(\"https://%s/%s\", addr, strings.TrimLeft(requestPath, \"/\"))\n\treq, err := http.NewRequestWithContext(execCtx.Context(), http.MethodHead, url, nil)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"failed to create http request: %w\", err)\n\t\tne.logger.Warn(err.Error())\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Host\", domain)\n\treq.Header.Set(\"User-Agent\", app.AppUserAgent)\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"failed to send http request: %w\", err)\n\t\tne.logger.Warn(err.Error())\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 {\n\t\treturn make([]*x509.Certificate, 0), nil\n\t}\n\treturn resp.TLS.PeerCertificates, nil\n}\n\nfunc (ne *bizMonitorNodeExecutor) setVariablesOfResult(execCtx *NodeExecutionContext, execRes *NodeExecutionResult, certX509 *x509.Certificate) {\n\tvar vCommonName string\n\tvar vSubjectAltNames string\n\tvar vNotBefore time.Time\n\tvar vNotAfter time.Time\n\tvar vHoursLeft int32\n\tvar vDaysLeft int32\n\tvar vValidity bool\n\n\tif certX509 != nil {\n\t\tvCommonName = certX509.Subject.CommonName\n\t\tvSubjectAltNames = strings.Join(xcertx509.GetSubjectAltNames(certX509), \";\")\n\t\tvNotBefore = certX509.NotBefore\n\t\tvNotAfter = certX509.NotAfter\n\t\tvHoursLeft = int32(math.Floor(time.Until(certX509.NotAfter).Hours()))\n\t\tvDaysLeft = int32(math.Floor(time.Until(certX509.NotAfter).Hours() / 24))\n\t\tvValidity = certX509.NotAfter.After(time.Now())\n\t}\n\n\texecRes.AddVariable(stateVarKeyCertificateDomain, vCommonName, stateValTypeString)\n\texecRes.AddVariable(stateVarKeyCertificateDomains, vSubjectAltNames, stateValTypeString)\n\texecRes.AddVariable(stateVarKeyCertificateCommonName, vCommonName, stateValTypeString)\n\texecRes.AddVariable(stateVarKeyCertificateSubjectAltNames, vSubjectAltNames, stateValTypeString)\n\texecRes.AddVariable(stateVarKeyCertificateNotBefore, vNotBefore, stateValTypeDateTime)\n\texecRes.AddVariable(stateVarKeyCertificateNotAfter, vNotAfter, stateValTypeDateTime)\n\texecRes.AddVariable(stateVarKeyCertificateHoursLeft, vHoursLeft, stateValTypeNumber)\n\texecRes.AddVariable(stateVarKeyCertificateDaysLeft, vDaysLeft, stateValTypeNumber)\n\texecRes.AddVariable(stateVarKeyCertificateValidity, vValidity, stateValTypeBoolean)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDomain, vCommonName, stateValTypeString)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDomains, vSubjectAltNames, stateValTypeString)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateCommonName, vCommonName, stateValTypeString)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateSubjectAltNames, vSubjectAltNames, stateValTypeString)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateNotBefore, vNotBefore, stateValTypeDateTime)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateNotAfter, vNotAfter, stateValTypeDateTime)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateHoursLeft, vHoursLeft, stateValTypeNumber)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDaysLeft, vDaysLeft, stateValTypeNumber)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, vValidity, stateValTypeBoolean)\n}\n\nfunc newBizMonitorNodeExecutor() NodeExecutor {\n\treturn &bizMonitorNodeExecutor{\n\t\tnodeExecutor:    nodeExecutor{logger: slog.Default()},\n\t\tcertificateRepo: repository.NewCertificateRepository(),\n\t}\n}\n"
  },
  {
    "path": "internal/workflow/engine/executor_biznotify.go",
    "content": "package engine\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/internal/notify\"\n\t\"github.com/certimate-go/certimate/internal/repository\"\n)\n\ntype bizNotifyNodeExecutor struct {\n\tnodeExecutor\n\n\taccessRepo   accessRepository\n\tsettingsRepo settingsRepository\n}\n\nfunc (ne *bizNotifyNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) {\n\texecRes := newNodeExecutionResult(execCtx.Node)\n\n\tnodeCfg := execCtx.Node.Data.Config.AsBizNotify()\n\tne.logger.Info(\"ready to send notification ...\", slog.Any(\"config\", nodeCfg))\n\n\t// 检测是否可以跳过本次执行\n\tif skippable, reason := ne.checkCanSkip(execCtx); skippable {\n\t\tne.logger.Info(fmt.Sprintf(\"skip this application, because %s\", reason))\n\t\treturn execRes, nil\n\t}\n\n\t// 读取部署提供商授权\n\tproviderAccessConfig := make(map[string]any)\n\tif nodeCfg.ProviderAccessId != \"\" {\n\t\tif access, err := ne.accessRepo.GetById(execCtx.Context(), nodeCfg.ProviderAccessId); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get access #%s record: %w\", nodeCfg.ProviderAccessId, err)\n\t\t} else {\n\t\t\tproviderAccessConfig = access.Config\n\t\t}\n\t}\n\n\t// 渲染通知模板\n\treMustache := regexp.MustCompile(`\\{\\{\\s*(\\$[^\\s]+)\\s*\\}\\}`)\n\treMustacheReplacer := func(match string) string {\n\t\tmustache := strings.TrimSpace(match[2 : len(match)-2])\n\t\tif mustache == \"\" {\n\t\t\treturn match\n\t\t}\n\n\t\tkey := mustache[1:]\n\t\tif key == \"\" {\n\t\t\treturn match\n\t\t} else if key == \"now\" {\n\t\t\treturn time.Now().Format(time.RFC3339)\n\t\t}\n\n\t\t// TODO: 支持作用域变量\n\t\tif state, ok := execCtx.variables.Get(key); ok {\n\t\t\treturn state.ValueString()\n\t\t}\n\n\t\treturn match\n\t}\n\tsubject := reMustache.ReplaceAllStringFunc(nodeCfg.Subject, reMustacheReplacer)\n\tmessage := reMustache.ReplaceAllStringFunc(nodeCfg.Message, reMustacheReplacer)\n\n\t// 推送通知\n\tnotifier := notify.NewClient(notify.WithLogger(ne.logger))\n\tnotifyReq := &notify.SendNotificationRequest{\n\t\tProvider:               nodeCfg.Provider,\n\t\tProviderAccessConfig:   providerAccessConfig,\n\t\tProviderExtendedConfig: nodeCfg.ProviderConfig,\n\t\tSubject:                subject,\n\t\tMessage:                message,\n\t}\n\tif _, err := notifier.SendNotification(execCtx.Context(), notifyReq); err != nil {\n\t\tne.logger.Warn(\"could not send notification\")\n\t\treturn execRes, err\n\t}\n\n\tne.logger.Info(\"notification completed\")\n\treturn execRes, nil\n}\n\nfunc (ne *bizNotifyNodeExecutor) checkCanSkip(execCtx *NodeExecutionContext) (_skip bool, _reason string) {\n\tthisNodeCfg := execCtx.Node.Data.Config.AsBizNotify()\n\tif !thisNodeCfg.SkipOnAllPrevSkipped {\n\t\treturn false, \"\"\n\t}\n\n\tvar total, skipped int32\n\tfor _, variable := range execCtx.variables.All() {\n\t\tif variable.Scope != \"\" && variable.Key == stateVarKeyNodeSkipped {\n\t\t\ttotal++\n\t\t\tif variable.Value == true {\n\t\t\t\tskipped++\n\t\t\t}\n\t\t}\n\t}\n\tif total == 0 || skipped != total {\n\t\treturn false, \"\"\n\t}\n\n\treturn true, \"all the previous nodes have been skipped\"\n}\n\nfunc newBizNotifyNodeExecutor() NodeExecutor {\n\treturn &bizNotifyNodeExecutor{\n\t\tnodeExecutor: nodeExecutor{logger: slog.Default()},\n\t\taccessRepo:   repository.NewAccessRepository(),\n\t\tsettingsRepo: repository.NewSettingsRepository(),\n\t}\n}\n"
  },
  {
    "path": "internal/workflow/engine/executor_bizupload.go",
    "content": "package engine\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/rsa\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"math\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/internal/repository\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\n/**\n * Outputs:\n *   - ref: \"certificate\": string\n *\n * Variables:\n *   - \"node.skipped\": boolean\n *   - \"certificate.commanName\": string\n *   - \"certificate.subjectAltNames\": string\n *   - \"certificate.notBefore\": datetime\n *   - \"certificate.notAfter\": datetime\n *   - \"certificate.hoursLeft\": number\n *   - \"certificate.daysLeft\": number\n *   - \"certificate.validity\": boolean\n */\ntype bizUploadNodeExecutor struct {\n\tnodeExecutor\n\n\tcertificateRepo certificateRepository\n\twfoutputRepo    workflowOutputRepository\n}\n\nconst (\n\tBizUploadSourceForm  = \"form\"\n\tBizUploadSourceLocal = \"local\"\n\tBizUploadSourceURL   = \"url\"\n)\n\nfunc (ne *bizUploadNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) {\n\texecRes := newNodeExecutionResult(execCtx.Node)\n\n\tnodeCfg := execCtx.Node.Data.Config.AsBizUpload()\n\tne.logger.Info(\"ready to upload certiticate ...\", slog.Any(\"config\", nodeCfg))\n\n\t// 查询上次执行结果\n\tlastOutput, lastCertificate, err := ne.getLastOutputArtifacts(execCtx)\n\tif err != nil {\n\t\treturn execRes, err\n\t} else {\n\t\tif lastOutput != nil {\n\t\t\tne.logger.Info(fmt.Sprintf(\"found last node output #%s record\", lastOutput.RunId))\n\t\t}\n\n\t\tif lastCertificate != nil {\n\t\t\tne.setOuputsOfResult(execCtx, execRes, lastCertificate, false)\n\t\t\tne.setVariablesOfResult(execCtx, execRes, lastCertificate)\n\t\t\tne.logger.Info(fmt.Sprintf(\"found last certificate #%s record\", lastCertificate.Id))\n\t\t}\n\t}\n\n\t// 检测是否可以跳过本次执行\n\tif skippable, reason := ne.checkCanSkip(execCtx, lastOutput, lastCertificate); skippable {\n\t\tne.logger.Info(fmt.Sprintf(\"skip this uploading, because %s\", reason))\n\n\t\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, true, stateValTypeBoolean)\n\t\treturn execRes, nil\n\t} else if reason != \"\" {\n\t\tne.logger.Info(fmt.Sprintf(\"re-upload, because %s\", reason))\n\t} else if lastCertificate != nil {\n\t\tne.logger.Info(\"no found last uploaded certificate, begin to upload\")\n\t} else {\n\t\tne.logger.Info(\"try to upload\")\n\t}\n\n\t// 获取证书及私钥\n\tvar certPEM, privkeyPEM string\n\tswitch nodeCfg.Source {\n\tcase BizUploadSourceForm:\n\t\t{\n\t\t\tcertPEM = nodeCfg.Certificate\n\t\t\tprivkeyPEM = nodeCfg.PrivateKey\n\t\t}\n\n\tcase BizUploadSourceLocal:\n\t\t{\n\t\t\tcertData, err := os.ReadFile(nodeCfg.Certificate)\n\t\t\tif err != nil {\n\t\t\t\treturn execRes, fmt.Errorf(\"failed to read certificate file from local path: %w\", err)\n\t\t\t} else {\n\t\t\t\tcertPEM = string(certData)\n\t\t\t}\n\n\t\t\tprivkeyData, err := os.ReadFile(nodeCfg.PrivateKey)\n\t\t\tif err != nil {\n\t\t\t\treturn execRes, fmt.Errorf(\"failed to read private key file from local path: %w\", err)\n\t\t\t} else {\n\t\t\t\tprivkeyPEM = string(privkeyData)\n\t\t\t}\n\t\t}\n\n\tcase BizUploadSourceURL:\n\t\t{\n\t\t\tclient := resty.New()\n\t\t\tclient.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})\n\n\t\t\tcertResp, err := client.NewRequest().Get(nodeCfg.Certificate)\n\t\t\tif err != nil || certResp.IsError() {\n\t\t\t\treturn execRes, fmt.Errorf(\"failed to download certificate from URL: %w\", err)\n\t\t\t} else {\n\t\t\t\tcertPEM = string(certResp.Body())\n\t\t\t}\n\n\t\t\tprivkeyResp, err := client.NewRequest().Get(nodeCfg.PrivateKey)\n\t\t\tif err != nil || privkeyResp.IsError() {\n\t\t\t\treturn execRes, fmt.Errorf(\"failed to download private key from URL: %w\", err)\n\t\t\t} else {\n\t\t\t\tprivkeyPEM = string(privkeyResp.Body())\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn execRes, fmt.Errorf(\"unsupported upload source: '%s'\", nodeCfg.Source)\n\t}\n\n\t// 验证证书\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn execRes, err\n\t} else if certX509.NotAfter.Before(time.Now()) {\n\t\tne.logger.Warn(fmt.Sprintf(\"the uploaded certificate has expired at %s\", certX509.NotAfter.UTC().Format(time.RFC3339)))\n\t}\n\n\t// 验证私钥\n\tprivkey, err := xcert.ParsePrivateKeyFromPEM(privkeyPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tmatched := false\n\t\tswitch pub := certX509.PublicKey.(type) {\n\t\tcase *rsa.PublicKey:\n\t\t\tp, ok := privkey.(*rsa.PrivateKey)\n\t\t\tmatched = ok && pub.Equal(p.Public())\n\t\tcase *ecdsa.PublicKey:\n\t\t\tp, ok := privkey.(*ecdsa.PrivateKey)\n\t\t\tmatched = ok && pub.Equal(p.Public())\n\t\tcase ed25519.PublicKey:\n\t\t\tp, ok := privkey.(ed25519.PrivateKey)\n\t\t\tmatched = ok && pub.Equal(p.Public())\n\t\tdefault:\n\t\t\tmatched = false\n\t\t}\n\n\t\tif !matched {\n\t\t\treturn nil, fmt.Errorf(\"the uploaded private key does not match the uploaded certificate\")\n\t\t}\n\t}\n\n\t// 二次检测是否可以跳过执行\n\tif lastCertificate != nil {\n\t\tif xcert.EqualCertificatesFromPEM(certPEM, lastCertificate.Certificate) {\n\t\t\tne.logger.Info(\"skip this uploading, because the last uploaded certificate already exists\")\n\t\t\treturn execRes, nil\n\t\t}\n\t}\n\n\t// 保存证书实体\n\tcertificate := &domain.Certificate{\n\t\tSource:         domain.CertificateSourceTypeUpload,\n\t\tWorkflowId:     execCtx.WorkflowId,\n\t\tWorkflowRunId:  execCtx.RunId,\n\t\tWorkflowNodeId: execCtx.Node.Id,\n\t}\n\tcertificate.PopulateFromPEM(certPEM, privkeyPEM)\n\tif certificate, err := ne.certificateRepo.Save(execCtx.Context(), certificate); err != nil {\n\t\tne.logger.Warn(\"could not save certificate\")\n\t\treturn execRes, err\n\t} else {\n\t\tne.logger.Info(\"certificate saved\", slog.String(\"recordId\", certificate.Id))\n\t}\n\n\t// 节点输出\n\tne.setOuputsOfResult(execCtx, execRes, certificate, true)\n\tne.setVariablesOfResult(execCtx, execRes, certificate)\n\n\tne.logger.Info(\"uploading completed\")\n\treturn execRes, nil\n}\n\nfunc (ne *bizUploadNodeExecutor) getLastOutputArtifacts(execCtx *NodeExecutionContext) (*domain.WorkflowOutput, *domain.Certificate, error) {\n\tlastOutput, err := ne.wfoutputRepo.GetByWorkflowIdAndNodeId(execCtx.Context(), execCtx.WorkflowId, execCtx.Node.Id)\n\tif err != nil && !domain.IsRecordNotFoundError(err) {\n\t\treturn nil, nil, fmt.Errorf(\"failed to get last output record of node #%s: %w\", execCtx.Node.Id, err)\n\t}\n\n\tif lastOutput != nil {\n\t\tlastCertificate, err := ne.certificateRepo.GetByWorkflowRunIdAndNodeId(execCtx.Context(), lastOutput.RunId, lastOutput.NodeId)\n\t\tif err != nil && !domain.IsRecordNotFoundError(err) {\n\t\t\treturn lastOutput, nil, fmt.Errorf(\"failed to get last certificate record of node #%s: %w\", execCtx.Node.Id, err)\n\t\t}\n\n\t\treturn lastOutput, lastCertificate, nil\n\t}\n\n\treturn lastOutput, nil, nil\n}\n\nfunc (ne *bizUploadNodeExecutor) checkCanSkip(execCtx *NodeExecutionContext, lastOutput *domain.WorkflowOutput, lastCertificate *domain.Certificate) (_skip bool, _reason string) {\n\tthisNodeCfg := execCtx.Node.Data.Config.AsBizUpload()\n\n\tif lastOutput != nil && lastOutput.Succeeded {\n\t\t// 比较和上次上传时的关键配置（即影响证书上传的）参数是否一致\n\t\tlastNodeCfg := lastOutput.NodeConfig.AsBizUpload()\n\n\t\tif thisNodeCfg.Source != lastNodeCfg.Source {\n\t\t\treturn false, \"the configuration item 'Source' changed\"\n\t\t}\n\n\t\tswitch thisNodeCfg.Source {\n\t\tcase BizUploadSourceForm:\n\t\t\tif strings.TrimSpace(thisNodeCfg.Certificate) != strings.TrimSpace(lastNodeCfg.Certificate) {\n\t\t\t\treturn false, \"the configuration item 'Certificate' changed\"\n\t\t\t}\n\t\t\tif strings.TrimSpace(thisNodeCfg.PrivateKey) != strings.TrimSpace(lastNodeCfg.PrivateKey) {\n\t\t\t\treturn false, \"the configuration item 'PrivateKey' changed\"\n\t\t\t}\n\n\t\tdefault:\n\t\t\t// 本地或远程文件来源，需实际下载后才能比较\n\t\t\treturn false, \"\"\n\t\t}\n\t}\n\n\tif lastCertificate != nil {\n\t\treturn true, \"the last uploaded certificate already exists\"\n\t}\n\n\treturn false, \"\"\n}\n\nfunc (ne *bizUploadNodeExecutor) setOuputsOfResult(execCtx *NodeExecutionContext, execRes *NodeExecutionResult, certificate *domain.Certificate, persistent bool) {\n\tif certificate != nil {\n\t\tkey := \"certificate\"\n\t\tvalue := fmt.Sprintf(\"%s#%s\", domain.CollectionNameCertificate, certificate.Id)\n\t\tif persistent {\n\t\t\texecRes.AddOutputWithPersistent(stateIOTypeRef, key, value, stateValTypeString)\n\t\t} else {\n\t\t\texecRes.AddOutput(stateIOTypeRef, key, value, stateValTypeString)\n\t\t}\n\t}\n}\n\nfunc (ne *bizUploadNodeExecutor) setVariablesOfResult(execCtx *NodeExecutionContext, execRes *NodeExecutionResult, certificate *domain.Certificate) {\n\tvar vCommonName string\n\tvar vSubjectAltNames string\n\tvar vNotBefore time.Time\n\tvar vNotAfter time.Time\n\tvar vHoursLeft int32\n\tvar vDaysLeft int32\n\tvar vValidity bool\n\n\tif certificate != nil {\n\t\tvCommonName = strings.Split(certificate.SubjectAltNames, \";\")[0]\n\t\tvSubjectAltNames = certificate.SubjectAltNames\n\t\tvNotBefore = certificate.ValidityNotBefore\n\t\tvNotAfter = certificate.ValidityNotAfter\n\t\tvHoursLeft = int32(math.Floor(time.Until(certificate.ValidityNotAfter).Hours()))\n\t\tvDaysLeft = int32(math.Floor(time.Until(certificate.ValidityNotAfter).Hours() / 24))\n\t\tvValidity = certificate.ValidityNotAfter.After(time.Now())\n\t}\n\n\texecRes.AddVariable(stateVarKeyCertificateDomain, vCommonName, stateValTypeString)\n\texecRes.AddVariable(stateVarKeyCertificateDomains, vSubjectAltNames, stateValTypeString)\n\texecRes.AddVariable(stateVarKeyCertificateCommonName, vCommonName, stateValTypeString)\n\texecRes.AddVariable(stateVarKeyCertificateSubjectAltNames, vSubjectAltNames, stateValTypeString)\n\texecRes.AddVariable(stateVarKeyCertificateNotBefore, vNotBefore, stateValTypeDateTime)\n\texecRes.AddVariable(stateVarKeyCertificateNotAfter, vNotAfter, stateValTypeDateTime)\n\texecRes.AddVariable(stateVarKeyCertificateHoursLeft, vHoursLeft, stateValTypeNumber)\n\texecRes.AddVariable(stateVarKeyCertificateDaysLeft, vDaysLeft, stateValTypeNumber)\n\texecRes.AddVariable(stateVarKeyCertificateValidity, vValidity, stateValTypeBoolean)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDomain, vCommonName, stateValTypeString)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDomains, vSubjectAltNames, stateValTypeString)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateCommonName, vCommonName, stateValTypeString)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateSubjectAltNames, vSubjectAltNames, stateValTypeString)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateNotBefore, vNotBefore, stateValTypeDateTime)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateNotAfter, vNotAfter, stateValTypeDateTime)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateHoursLeft, vHoursLeft, stateValTypeNumber)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDaysLeft, vDaysLeft, stateValTypeNumber)\n\texecRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, vValidity, stateValTypeBoolean)\n}\n\nfunc newBizUploadNodeExecutor() NodeExecutor {\n\treturn &bizUploadNodeExecutor{\n\t\tnodeExecutor:    nodeExecutor{logger: slog.Default()},\n\t\tcertificateRepo: repository.NewCertificateRepository(),\n\t\twfoutputRepo:    repository.NewWorkflowOutputRepository(),\n\t}\n}\n"
  },
  {
    "path": "internal/workflow/engine/executor_condition.go",
    "content": "package engine\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/samber/lo\"\n)\n\ntype conditionNodeExecutor struct {\n\tnodeExecutor\n}\n\nfunc (ne *conditionNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) {\n\tvar engine *workflowEngine\n\tif we, ok := execCtx.engine.(*workflowEngine); !ok {\n\t\tpanic(\"unreachable\")\n\t} else {\n\t\tengine = we\n\t}\n\n\texecRes := newNodeExecutionResult(execCtx.Node)\n\n\terrs := make([]error, 0)\n\tblocks := lo.Filter(execCtx.Node.Blocks, func(n *Node, _ int) bool { return n.Type == NodeTypeBranchBlock })\n\tfor _, node := range blocks {\n\t\tctx := execCtx.Context()\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn execRes, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\terr := engine.executeNode(execCtx.Clone(), node)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, ErrTerminated) {\n\t\t\t\treturn execRes, err\n\t\t\t}\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn execRes, fmt.Errorf(\"%w: %w\", ErrBlocksException, errors.Join(errs...))\n\t}\n\n\treturn execRes, nil\n}\n\nfunc newConditionNodeExecutor() NodeExecutor {\n\treturn &conditionNodeExecutor{\n\t\tnodeExecutor: nodeExecutor{logger: slog.Default()},\n\t}\n}\n\ntype branchBlockNodeExecutor struct {\n\tnodeExecutor\n}\n\nfunc (ne *branchBlockNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) {\n\texecRes := newNodeExecutionResult(execCtx.Node)\n\n\tnodeCfg := execCtx.Node.Data.Config.AsBranchBlock()\n\tif nodeCfg.Expression == nil {\n\t\tne.logger.Info(\"enter this branch without any conditions\")\n\t} else {\n\t\tvariables := lo.Reduce(execCtx.variables.All(), func(acc map[string]map[string]any, state VariableState, _ int) map[string]map[string]any {\n\t\t\tif _, ok := acc[state.Scope]; !ok {\n\t\t\t\tacc[state.Scope] = make(map[string]any)\n\t\t\t}\n\n\t\t\t// 这里需要把所有值都转换为字符串形式，因为 Expression.Eval 仅支持字符串类型的值\n\t\t\tacc[state.Scope][state.Key] = state.ValueString()\n\t\t\treturn acc\n\t\t}, make(map[string]map[string]any))\n\n\t\trs, err := nodeCfg.Expression.Eval(variables)\n\t\tif err != nil {\n\t\t\tne.logger.Warn(fmt.Sprintf(\"failed to eval expr: %+v\", err))\n\t\t\treturn execRes, err\n\t\t}\n\n\t\tif rs.Value == false {\n\t\t\tne.logger.Info(\"skip this branch, because condition not met\")\n\t\t\treturn execRes, nil\n\t\t} else {\n\t\t\tne.logger.Info(\"enter this branch, because condition met\")\n\t\t}\n\t}\n\n\tif engine, ok := execCtx.engine.(*workflowEngine); !ok {\n\t\tpanic(\"unreachable\")\n\t} else {\n\t\tif err := engine.executeBlocks(execCtx.Clone(), execCtx.Node.Blocks); err != nil {\n\t\t\treturn execRes, fmt.Errorf(\"%w: %w\", ErrBlocksException, err)\n\t\t}\n\t}\n\n\treturn execRes, nil\n}\n\nfunc newBranchBlockNodeExecutor() NodeExecutor {\n\treturn &branchBlockNodeExecutor{\n\t\tnodeExecutor: nodeExecutor{logger: slog.Default()},\n\t}\n}\n"
  },
  {
    "path": "internal/workflow/engine/executor_delay.go",
    "content": "package engine\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\txwait \"github.com/certimate-go/certimate/pkg/utils/wait\"\n)\n\ntype delayNodeExecutor struct {\n\tnodeExecutor\n}\n\nfunc (ne *delayNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) {\n\texecRes := newNodeExecutionResult(execCtx.Node)\n\n\tnodeCfg := execCtx.Node.Data.Config.AsDelay()\n\tne.logger.Info(fmt.Sprintf(\"delay for %d second(s) before continuing ...\", nodeCfg.Wait))\n\n\txwait.DelayWithContext(execCtx.Context(), time.Duration(nodeCfg.Wait)*time.Second)\n\n\treturn execRes, nil\n}\n\nfunc newDelayNodeExecutor() NodeExecutor {\n\treturn &delayNodeExecutor{\n\t\tnodeExecutor: nodeExecutor{logger: slog.Default()},\n\t}\n}\n"
  },
  {
    "path": "internal/workflow/engine/executor_end.go",
    "content": "package engine\n\nimport (\n\t\"log/slog\"\n)\n\ntype endNodeExecutor struct {\n\tnodeExecutor\n}\n\nfunc (ne *endNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) {\n\texecRes := newNodeExecutionResult(execCtx.Node)\n\texecRes.Terminated = true\n\n\tne.logger.Info(\"the workflow is ending\")\n\n\treturn execRes, nil\n}\n\nfunc newEndNodeExecutor() NodeExecutor {\n\treturn &endNodeExecutor{\n\t\tnodeExecutor: nodeExecutor{logger: slog.Default()},\n\t}\n}\n"
  },
  {
    "path": "internal/workflow/engine/executor_start.go",
    "content": "package engine\n\nimport (\n\t\"log/slog\"\n)\n\ntype startNodeExecutor struct {\n\tnodeExecutor\n}\n\nfunc (ne *startNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) {\n\texecRes := newNodeExecutionResult(execCtx.Node)\n\n\tne.logger.Info(\"the workflow is starting\")\n\n\treturn execRes, nil\n}\n\nfunc newStartNodeExecutor() NodeExecutor {\n\treturn &startNodeExecutor{\n\t\tnodeExecutor: nodeExecutor{logger: slog.Default()},\n\t}\n}\n"
  },
  {
    "path": "internal/workflow/engine/executor_trycatch.go",
    "content": "package engine\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/samber/lo\"\n)\n\ntype tryCatchNodeExecutor struct {\n\tnodeExecutor\n}\n\nfunc (ne *tryCatchNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) {\n\tvar engine *workflowEngine\n\tif we, ok := execCtx.engine.(*workflowEngine); !ok {\n\t\tpanic(\"unreachable\")\n\t} else {\n\t\tengine = we\n\t}\n\n\texecRes := newNodeExecutionResult(execCtx.Node)\n\n\ttryErrs := make([]error, 0)\n\ttryBlocks := lo.Filter(execCtx.Node.Blocks, func(n *Node, _ int) bool { return n.Type == NodeTypeTryBlock })\n\tfor _, node := range tryBlocks {\n\t\tctx := execCtx.Context()\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn execRes, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\terr := engine.executeNode(execCtx.Clone(), node)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, ErrTerminated) {\n\t\t\t\treturn execRes, err\n\t\t\t}\n\t\t\ttryErrs = append(tryErrs, err)\n\t\t}\n\t}\n\n\tif len(tryErrs) > 0 {\n\t\tcatchErrs := make([]error, 0)\n\t\tcatchBlocks := lo.Filter(execCtx.Node.Blocks, func(n *Node, _ int) bool { return n.Type == NodeTypeCatchBlock })\n\t\tfor _, node := range catchBlocks {\n\t\t\tselect {\n\t\t\tcase <-execCtx.Context().Done():\n\t\t\t\treturn execRes, execCtx.Context().Err()\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\terr := engine.executeNode(execCtx.Clone(), node)\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, ErrTerminated) {\n\t\t\t\t\treturn execRes, err\n\t\t\t\t}\n\t\t\t\tcatchErrs = append(catchErrs, err)\n\t\t\t}\n\t\t}\n\n\t\terrs := make([]error, 0)\n\t\terrs = append(errs, tryErrs...)\n\t\terrs = append(errs, catchErrs...)\n\t\treturn execRes, fmt.Errorf(\"%w: %w\", ErrBlocksException, errors.Join(errs...))\n\t}\n\n\treturn execRes, nil\n}\n\nfunc newTryCatchNodeExecutor() NodeExecutor {\n\treturn &tryCatchNodeExecutor{\n\t\tnodeExecutor: nodeExecutor{logger: slog.Default()},\n\t}\n}\n\ntype tryBlockNodeExecutor struct {\n\tnodeExecutor\n}\n\nfunc (ne *tryBlockNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) {\n\tvar engine *workflowEngine\n\tif we, ok := execCtx.engine.(*workflowEngine); !ok {\n\t\tpanic(\"unreachable\")\n\t} else {\n\t\tengine = we\n\t}\n\n\texecRes := newNodeExecutionResult(execCtx.Node)\n\n\tif err := engine.executeBlocks(execCtx.Clone(), execCtx.Node.Blocks); err != nil {\n\t\treturn execRes, fmt.Errorf(\"%w: %w\", ErrBlocksException, err)\n\t}\n\n\treturn execRes, nil\n}\n\nfunc newTryBlockNodeExecutor() NodeExecutor {\n\treturn &tryBlockNodeExecutor{\n\t\tnodeExecutor: nodeExecutor{logger: slog.Default()},\n\t}\n}\n\ntype catchBlockNodeExecutor struct {\n\tnodeExecutor\n}\n\nfunc (ne *catchBlockNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) {\n\texecRes := newNodeExecutionResult(execCtx.Node)\n\n\tvar engine *workflowEngine\n\tif we, ok := execCtx.engine.(*workflowEngine); !ok {\n\t\tpanic(\"unreachable\")\n\t} else {\n\t\tengine = we\n\t}\n\n\tif err := engine.executeBlocks(execCtx.Clone(), execCtx.Node.Blocks); err != nil {\n\t\treturn execRes, fmt.Errorf(\"%w: %w\", ErrBlocksException, err)\n\t}\n\n\treturn execRes, nil\n}\n\nfunc newCatchBlockNodeExecutor() NodeExecutor {\n\treturn &catchBlockNodeExecutor{\n\t\tnodeExecutor: nodeExecutor{logger: slog.Default()},\n\t}\n}\n"
  },
  {
    "path": "internal/workflow/engine/logger.go",
    "content": "package engine\n\nimport (\n\t\"log/slog\"\n)\n\ntype withLogger interface {\n\tSetLogger(logger *slog.Logger)\n}\n"
  },
  {
    "path": "internal/workflow/engine/models.go",
    "content": "package engine\n\nimport (\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\ntype Node = domain.WorkflowNode\n\ntype NodeType = domain.WorkflowNodeType\n\nconst (\n\tNodeTypeStart       = domain.WorkflowNodeTypeStart\n\tNodeTypeEnd         = domain.WorkflowNodeTypeEnd\n\tNodeTypeCondition   = domain.WorkflowNodeTypeCondition\n\tNodeTypeBranchBlock = domain.WorkflowNodeTypeBranchBlock\n\tNodeTypeTryCatch    = domain.WorkflowNodeTypeTryCatch\n\tNodeTypeTryBlock    = domain.WorkflowNodeTypeTryBlock\n\tNodeTypeCatchBlock  = domain.WorkflowNodeTypeCatchBlock\n\tNodeTypeDelay       = domain.WorkflowNodeTypeDelay\n\tNodeTypeBizApply    = domain.WorkflowNodeTypeBizApply\n\tNodeTypeBizUpload   = domain.WorkflowNodeTypeBizUpload\n\tNodeTypeBizMonitor  = domain.WorkflowNodeTypeBizMonitor\n\tNodeTypeBizDeploy   = domain.WorkflowNodeTypeBizDeploy\n\tNodeTypeBizNotify   = domain.WorkflowNodeTypeBizNotify\n)\n\ntype Graph = domain.WorkflowGraph\n"
  },
  {
    "path": "internal/workflow/engine/state.go",
    "content": "package engine\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n)\n\ntype VariableState struct {\n\tScope     string // 零值时表示全局的，否则表示指定节点的\n\tKey       string\n\tValue     any\n\tValueType string\n}\n\nfunc (s VariableState) ValueString() string {\n\tswitch s.ValueType {\n\tcase stateValTypeString:\n\t\treturn fmt.Sprintf(\"%s\", s.Value)\n\tcase stateValTypeNumber:\n\t\treturn fmt.Sprintf(\"%d\", s.Value)\n\tcase stateValTypeBoolean:\n\t\treturn strconv.FormatBool(s.Value.(bool))\n\tcase stateValTypeDateTime:\n\t\tvalueAsTime := s.Value.(time.Time)\n\t\tif valueAsTime.IsZero() {\n\t\t\treturn \"-\"\n\t\t}\n\t\treturn valueAsTime.Format(time.RFC3339)\n\tdefault:\n\t\treturn fmt.Sprintf(\"[%s]%v\", s.ValueType, s.Value)\n\t}\n}\n\ntype VariableManager interface {\n\tAll() []VariableState\n\tErase()\n\n\tAdd(entry VariableState)\n\tSet(name string, value any, valueType string)\n\tSetScoped(scope string, name string, value any, valueType string)\n\tGet(name string) (*VariableState, bool)\n\tGetScoped(scope string, key string) (*VariableState, bool)\n\tTake(key string) (*VariableState, bool)\n\tTakeScoped(scope string, key string) (*VariableState, bool)\n\tRemove(key string) bool\n\tRemoveScoped(scope string, key string) bool\n}\n\ntype variableManager struct {\n\tstatesMtx sync.RWMutex\n\tstates    []VariableState\n}\n\nvar _ VariableManager = (*variableManager)(nil)\n\nfunc (m *variableManager) All() []VariableState {\n\tm.statesMtx.RLock()\n\tdefer m.statesMtx.RUnlock()\n\n\tif m.states == nil {\n\t\treturn make([]VariableState, 0)\n\t}\n\n\treturn slices.Clone(m.states)\n}\n\nfunc (m *variableManager) Erase() {\n\tm.statesMtx.Lock()\n\tdefer m.statesMtx.Unlock()\n\n\tm.states = make([]VariableState, 0)\n}\n\nfunc (m *variableManager) Add(state VariableState) {\n\tm.statesMtx.Lock()\n\tdefer m.statesMtx.Unlock()\n\n\tif m.states == nil {\n\t\tm.states = make([]VariableState, 0)\n\t}\n\n\tfor i, item := range m.states {\n\t\tif item.Scope == state.Scope && item.Key == state.Key {\n\t\t\tm.states[i] = state\n\t\t\treturn\n\t\t}\n\t}\n\tm.states = append(m.states, state)\n}\n\nfunc (m *variableManager) Set(key string, value any, valueType string) {\n\tm.SetScoped(\"\", key, value, valueType)\n}\n\nfunc (m *variableManager) SetScoped(scope string, key string, value any, valueType string) {\n\tm.Add(VariableState{Scope: scope, Key: key, Value: value, ValueType: valueType})\n}\n\nfunc (m *variableManager) Get(key string) (*VariableState, bool) {\n\treturn m.GetScoped(\"\", key)\n}\n\nfunc (m *variableManager) GetScoped(scope string, key string) (*VariableState, bool) {\n\tm.statesMtx.RLock()\n\tdefer m.statesMtx.RUnlock()\n\n\tif m.states == nil {\n\t\treturn nil, false\n\t}\n\n\tfor _, item := range m.states {\n\t\tif item.Scope == scope && item.Key == key {\n\t\t\treturn &item, true\n\t\t}\n\t}\n\treturn nil, false\n}\n\nfunc (m *variableManager) Take(key string) (*VariableState, bool) {\n\treturn m.TakeScoped(\"\", key)\n}\n\nfunc (m *variableManager) TakeScoped(scope string, key string) (*VariableState, bool) {\n\tm.statesMtx.Lock()\n\tdefer m.statesMtx.Unlock()\n\n\tif m.states == nil {\n\t\treturn nil, false\n\t}\n\n\tfor i, item := range m.states {\n\t\tif item.Scope == scope && item.Key == key {\n\t\t\tm.states = slices.Delete(m.states, i, i+1)\n\t\t\treturn &item, true\n\t\t}\n\t}\n\treturn nil, false\n}\n\nfunc (m *variableManager) Remove(key string) bool {\n\treturn m.RemoveScoped(\"\", key)\n}\n\nfunc (m *variableManager) RemoveScoped(scope string, key string) bool {\n\t_, ok := m.TakeScoped(scope, key)\n\treturn ok\n}\n\nfunc newVariableManager() VariableManager {\n\treturn &variableManager{\n\t\tstates: make([]VariableState, 0),\n\t}\n}\n\ntype InOutState struct {\n\tNodeId     string\n\tType       string\n\tName       string\n\tValue      any\n\tValueType  string\n\tPersistent bool\n}\n\nfunc (s InOutState) ValueString() string {\n\tswitch s.ValueType {\n\tcase stateValTypeString:\n\t\treturn s.Value.(string)\n\tcase stateValTypeNumber:\n\t\treturn fmt.Sprintf(\"%d\", s.Value)\n\tcase stateValTypeBoolean:\n\t\treturn strconv.FormatBool(s.Value.(bool))\n\tdefault:\n\t\treturn fmt.Sprintf(\"%v\", s.Value)\n\t}\n}\n\ntype InOutManager interface {\n\tAll() []InOutState\n\tErase()\n\n\tAdd(state InOutState)\n\tSet(nodeId string, stype string, name string, value any, valueType string, persistent bool)\n\tGet(nodeId string, name string) (*InOutState, bool)\n\tTake(nodeId string, name string) (*InOutState, bool)\n\tRemove(nodeId string, name string) bool\n}\n\ntype inoutManager struct {\n\tstatesMtx sync.RWMutex\n\tstates    []InOutState\n}\n\nvar _ InOutManager = (*inoutManager)(nil)\n\nfunc (m *inoutManager) All() []InOutState {\n\tm.statesMtx.RLock()\n\tdefer m.statesMtx.RUnlock()\n\n\tif m.states == nil {\n\t\treturn make([]InOutState, 0)\n\t}\n\n\treturn slices.Clone(m.states)\n}\n\nfunc (m *inoutManager) Erase() {\n\tm.statesMtx.Lock()\n\tdefer m.statesMtx.Unlock()\n\n\tm.states = make([]InOutState, 0)\n}\n\nfunc (m *inoutManager) Add(state InOutState) {\n\tm.statesMtx.Lock()\n\tdefer m.statesMtx.Unlock()\n\n\tif m.states == nil {\n\t\tm.states = make([]InOutState, 0)\n\t}\n\n\tfor i, item := range m.states {\n\t\tif item.NodeId == state.NodeId && item.Name == state.Name {\n\t\t\tm.states[i] = state\n\t\t\treturn\n\t\t}\n\t}\n\tm.states = append(m.states, state)\n}\n\nfunc (m *inoutManager) Set(nodeId string, stype string, name string, value any, valueType string, persistent bool) {\n\tm.Add(InOutState{NodeId: nodeId, Type: stype, Name: name, Value: value, ValueType: valueType, Persistent: persistent})\n}\n\nfunc (m *inoutManager) Get(nodeId string, name string) (*InOutState, bool) {\n\tm.statesMtx.RLock()\n\tdefer m.statesMtx.RUnlock()\n\n\tif m.states == nil {\n\t\treturn nil, false\n\t}\n\n\tfor _, item := range m.states {\n\t\tif item.NodeId == nodeId && item.Name == name {\n\t\t\treturn &item, true\n\t\t}\n\t}\n\treturn nil, false\n}\n\nfunc (m *inoutManager) Take(nodeId string, name string) (*InOutState, bool) {\n\tm.statesMtx.Lock()\n\tdefer m.statesMtx.Unlock()\n\n\tif m.states == nil {\n\t\treturn nil, false\n\t}\n\n\tfor i, item := range m.states {\n\t\tif item.NodeId == nodeId && item.Name == name {\n\t\t\tm.states = slices.Delete(m.states, i, i+1)\n\t\t\treturn &item, true\n\t\t}\n\t}\n\treturn nil, false\n}\n\nfunc (m *inoutManager) Remove(nodeId string, name string) bool {\n\t_, ok := m.Take(nodeId, name)\n\treturn ok\n}\n\nfunc newInOutManager() InOutManager {\n\treturn &inoutManager{\n\t\tstates: make([]InOutState, 0),\n\t}\n}\n\nconst (\n\tstateValTypeBoolean  = \"boolean\"\n\tstateValTypeDateTime = \"datetime\"\n\tstateValTypeNumber   = \"number\"\n\tstateValTypeString   = \"string\"\n)\n\nconst (\n\tstateIOTypeRef = \"ref\"\n)\n\nconst (\n\tstateVarKeyWorkflowId                 = \"workflow.id\"                 // ValueType: \"string\"\n\tstateVarKeyWorkflowName               = \"workflow.name\"               // ValueType: \"string\"\n\tstateVarKeyRunId                      = \"run.id\"                      // ValueType: \"string\"\n\tstateVarKeyRunTrigger                 = \"run.trigger\"                 // ValueType: \"string\"\n\tstateVarKeyNodeId                     = \"node.id\"                     // ValueType: \"string\"\n\tstateVarKeyNodeName                   = \"node.name\"                   // ValueType: \"string\"\n\tstateVarKeyNodeSkipped                = \"node.skipped\"                // ValueType: \"boolean\"\n\tstateVarKeyErrorNodeId                = \"error.nodeId\"                // ValueType: \"string\"\n\tstateVarKeyErrorNodeName              = \"error.nodeName\"              // ValueType: \"string\"\n\tstateVarKeyErrorMessage               = \"error.message\"               // ValueType: \"string\"\n\tstateVarKeyCertificateDomain          = \"certificate.domain\"          // 已废弃，仅为兼容旧版而保留，请使用 [stateVarKeyCertificateCommonName]\n\tstateVarKeyCertificateDomains         = \"certificate.domains\"         // 已废弃，仅为兼容旧版而保留，请使用 [stateVarKeyCertificateSubjectAltNames]\n\tstateVarKeyCertificateCommonName      = \"certificate.commonName\"      // ValueType: \"string\"\n\tstateVarKeyCertificateSubjectAltNames = \"certificate.subjectAltNames\" // ValueType: \"string\"\n\tstateVarKeyCertificateNotBefore       = \"certificate.notBefore\"       // ValueType: \"datetime\"\n\tstateVarKeyCertificateNotAfter        = \"certificate.notAfter\"        // ValueType: \"datetime\"\n\tstateVarKeyCertificateHoursLeft       = \"certificate.hoursLeft\"       // ValueType: \"number\"\n\tstateVarKeyCertificateDaysLeft        = \"certificate.daysLeft\"        // ValueType: \"number\"\n\tstateVarKeyCertificateValidity        = \"certificate.validity\"        // ValueType: \"boolean\"\n)\n"
  },
  {
    "path": "internal/workflow/pbhook.go",
    "content": "package workflow\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\nfunc registerWorkflowRecordEvents() {\n\tpb := app.GetApp()\n\tpb.OnRecordCreateRequest(domain.CollectionNameWorkflow).BindFunc(func(e *core.RecordRequestEvent) error {\n\t\tif err := e.Next(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := onWorkflowRecordCreateOrUpdate(e.Request.Context(), e.Record); err != nil {\n\t\t\tapp.GetLogger().Error(err.Error())\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t})\n\tpb.OnRecordUpdateRequest(domain.CollectionNameWorkflow).BindFunc(func(e *core.RecordRequestEvent) error {\n\t\tif err := e.Next(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := onWorkflowRecordCreateOrUpdate(e.Request.Context(), e.Record); err != nil {\n\t\t\tapp.GetLogger().Error(err.Error())\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t})\n\tpb.OnRecordDeleteRequest(domain.CollectionNameWorkflow).BindFunc(func(e *core.RecordRequestEvent) error {\n\t\tif err := e.Next(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := onWorkflowRecordDelete(e.Request.Context(), e.Record); err != nil {\n\t\t\tapp.GetLogger().Error(err.Error())\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\nfunc onWorkflowRecordCreateOrUpdate(_ context.Context, record *core.Record) error {\n\tscheduler := app.GetScheduler()\n\n\t// 向数据库插入/更新时，同时更新定时任务\n\tenabled := record.GetBool(\"enabled\")\n\ttrigger := record.GetString(\"trigger\")\n\ttriggerCron := record.GetString(\"triggerCron\")\n\n\t// 如果非定时触发或未启用，移除定时任务\n\tif !enabled || trigger != string(domain.WorkflowTriggerTypeScheduled) {\n\t\tscheduler.Remove(fmt.Sprintf(\"workflow#%s\", record.Id))\n\t\treturn nil\n\t}\n\n\t// 反之，重新添加定时任务\n\tif err := registerWorkflowJob(thisSvcInst(), record.Id, triggerCron); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc onWorkflowRecordDelete(_ context.Context, record *core.Record) error {\n\tscheduler := app.GetScheduler()\n\n\t// 从数据库删除时，同时移除定时任务\n\tjobId := fmt.Sprintf(\"workflow#%s\", record.Id)\n\tscheduler.Remove(jobId)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/workflow/pbjob.go",
    "content": "﻿package workflow\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/pocketbase/pocketbase/tools/cron\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/internal/domain/dtos\"\n)\n\nfunc registerWorkflowJob(workflowSrv *WorkflowService, workflowId string, triggerCron string) error {\n\tscheduler := app.GetScheduler()\n\n\tjobId := fmt.Sprintf(\"workflow#%s\", workflowId)\n\tjob, _ := lo.Find(scheduler.Jobs(), func(j *cron.Job) bool { return j.Id() == jobId })\n\tif job != nil && job.Expression() == triggerCron {\n\t\treturn nil\n\t}\n\n\terr := scheduler.Add(jobId, triggerCron, func() {\n\t\tapp.GetLogger().Info(fmt.Sprintf(\"workflow #%s is triggered ...\", workflowId))\n\n\t\t_, err := workflowSrv.StartRun(context.Background(), &dtos.WorkflowStartRunReq{\n\t\t\tWorkflowId: workflowId,\n\t\t\tRunTrigger: domain.WorkflowTriggerTypeScheduled,\n\t\t})\n\t\tif err != nil {\n\t\t\tapp.GetLogger().Warn(fmt.Sprintf(\"failed to start scheduled run for workflow #%s\", workflowId), slog.Any(\"error\", err))\n\t\t}\n\t})\n\tif err != nil {\n\t\tapp.GetLogger().Error(fmt.Sprintf(\"failed to register cron job for workflow #%s\", workflowId), slog.Any(\"error\", err))\n\t\treturn fmt.Errorf(\"failed to add cron job: %w\", err)\n\t}\n\n\tapp.GetLogger().Info(fmt.Sprintf(\"registered cron job for workflow #%s\", workflowId), slog.String(\"cron\", triggerCron))\n\treturn nil\n}\n"
  },
  {
    "path": "internal/workflow/service.go",
    "content": "package workflow\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/pocketbase/dbx\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/domain\"\n\t\"github.com/certimate-go/certimate/internal/domain/dtos\"\n\t\"github.com/certimate-go/certimate/internal/workflow/dispatcher\"\n)\n\ntype WorkflowService struct {\n\tdispatcher dispatcher.WorkflowDispatcher\n\n\tworkflowRepo    workflowRepository\n\tworkflowRunRepo workflowRunRepository\n\tsettingsRepo    settingsRepository\n}\n\nfunc NewWorkflowService(workflowRepo workflowRepository, workflowRunRepo workflowRunRepository, settingsRepo settingsRepository) *WorkflowService {\n\tsrv := &WorkflowService{\n\t\tdispatcher: dispatcher.GetSingletonDispatcher(),\n\n\t\tworkflowRepo:    workflowRepo,\n\t\tworkflowRunRepo: workflowRunRepo,\n\t\tsettingsRepo:    settingsRepo,\n\t}\n\treturn srv\n}\n\nfunc (s *WorkflowService) InitSchedule(ctx context.Context) error {\n\t// 每日清理工作流运行历史\n\tapp.GetScheduler().MustAdd(\"cleanupWorkflowHistoryRuns\", \"0 0 * * *\", func() {\n\t\ts.cleanupHistoryRuns(context.Background())\n\t})\n\n\t// 初始化工作流调度器\n\tif err := s.dispatcher.Bootup(ctx); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// 注册工作流后台任务\n\t{\n\t\tworkflows, err := s.workflowRepo.ListEnabledScheduled(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar errs []error\n\t\tfor _, workflow := range workflows {\n\t\t\tif err := registerWorkflowJob(s, workflow.Id, workflow.TriggerCron); err != nil {\n\t\t\t\terrs = append(errs, err)\n\t\t\t}\n\t\t}\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s *WorkflowService) GetStatistics(ctx context.Context) (*dtos.WorkflowStatisticsResp, error) {\n\tstats := s.dispatcher.GetStatistics()\n\treturn &dtos.WorkflowStatisticsResp{\n\t\tConcurrency:      stats.Concurrency,\n\t\tPendingRunIds:    stats.PendingRunIds,\n\t\tProcessingRunIds: stats.ProcessingRunIds,\n\t}, nil\n}\n\nfunc (s *WorkflowService) StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) (*dtos.WorkflowStartRunResp, error) {\n\tworkflow, err := s.workflowRepo.GetById(ctx, req.WorkflowId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif req.RunTrigger == domain.WorkflowTriggerTypeManual && (workflow.LastRunStatus == domain.WorkflowRunStatusTypePending || workflow.LastRunStatus == domain.WorkflowRunStatusTypeProcessing) {\n\t\treturn nil, errors.New(\"workflow is already pending or processing\")\n\t} else if workflow.GraphContent == nil {\n\t\treturn nil, errors.New(\"workflow graph content is empty\")\n\t} else if err := workflow.GraphContent.Verify(); err != nil {\n\t\treturn nil, fmt.Errorf(\"workflow graph content is invalid: %w\", err)\n\t}\n\n\tworkflowRun := &domain.WorkflowRun{\n\t\tWorkflowId: workflow.Id,\n\t\tStatus:     domain.WorkflowRunStatusTypePending,\n\t\tTrigger:    req.RunTrigger,\n\t\tStartedAt:  time.Now(),\n\t\tGraph:      workflow.GraphContent.Clone(),\n\t}\n\tif resp, err := s.workflowRunRepo.Save(ctx, workflowRun); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tworkflowRun = resp\n\t}\n\n\tif err := s.dispatcher.Start(ctx, workflowRun.Id); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &dtos.WorkflowStartRunResp{RunId: workflowRun.Id}, nil\n}\n\nfunc (s *WorkflowService) CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) (*dtos.WorkflowCancelRunResp, error) {\n\tworkflow, err := s.workflowRepo.GetById(ctx, req.WorkflowId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tworkflowRun, err := s.workflowRunRepo.GetById(ctx, req.RunId)\n\tif err != nil {\n\t\treturn nil, err\n\t} else if workflowRun.WorkflowId != workflow.Id {\n\t\treturn nil, errors.New(\"workflow run not found\")\n\t} else if workflowRun.Status != domain.WorkflowRunStatusTypePending && workflowRun.Status != domain.WorkflowRunStatusTypeProcessing {\n\t\treturn nil, errors.New(\"workflow run is not pending or processing\")\n\t}\n\n\tif err := s.dispatcher.Cancel(ctx, workflowRun.Id); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &dtos.WorkflowCancelRunResp{}, nil\n}\n\nfunc (s *WorkflowService) Shutdown(ctx context.Context) {\n\ts.dispatcher.Shutdown(ctx)\n}\n\nfunc (s *WorkflowService) cleanupHistoryRuns(ctx context.Context) error {\n\tsettings, err := s.settingsRepo.GetByName(ctx, domain.SettingsNamePersistence)\n\tif err != nil {\n\t\tif errors.Is(err, domain.ErrRecordNotFound) {\n\t\t\treturn nil\n\t\t}\n\n\t\tapp.GetLogger().Error(\"failed to get persistence settings\", slog.Any(\"error\", err))\n\t\treturn err\n\t}\n\n\tpersistenceSettings := settings.Content.AsPersistence()\n\tif persistenceSettings.WorkflowRunsRetentionMaxDays != 0 {\n\t\tret, err := s.workflowRunRepo.DeleteWhere(\n\t\t\tctx,\n\t\t\tdbx.NewExp(fmt.Sprintf(\"status!='%s'\", string(domain.WorkflowRunStatusTypePending))),\n\t\t\tdbx.NewExp(fmt.Sprintf(\"status!='%s'\", string(domain.WorkflowRunStatusTypeProcessing))),\n\t\t\tdbx.NewExp(fmt.Sprintf(\"endedAt<DATETIME('now', '-%d days')\", persistenceSettings.WorkflowRunsRetentionMaxDays)),\n\t\t)\n\t\tif err != nil {\n\t\t\tapp.GetLogger().Error(\"failed to delete workflow history runs\", slog.Any(\"error\", err))\n\t\t\treturn err\n\t\t}\n\n\t\tif ret > 0 {\n\t\t\tapp.GetLogger().Info(fmt.Sprintf(\"cleanup %d workflow history runs\", ret))\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/workflow/service_deps.go",
    "content": "package workflow\n\nimport (\n\t\"context\"\n\n\t\"github.com/pocketbase/dbx\"\n\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\ntype workflowRepository interface {\n\tListEnabledScheduled(ctx context.Context) ([]*domain.Workflow, error)\n\tGetById(ctx context.Context, id string) (*domain.Workflow, error)\n\tSave(ctx context.Context, workflow *domain.Workflow) (*domain.Workflow, error)\n}\n\ntype workflowRunRepository interface {\n\tGetById(ctx context.Context, id string) (*domain.WorkflowRun, error)\n\tSave(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error)\n\tSaveWithCascading(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error)\n\tDeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error)\n}\n\ntype settingsRepository interface {\n\tGetByName(ctx context.Context, name string) (*domain.Settings, error)\n}\n"
  },
  {
    "path": "internal/workflow/service_inst.go",
    "content": "﻿package workflow\n\nimport (\n\t\"sync\"\n\n\t\"github.com/certimate-go/certimate/internal/repository\"\n)\n\nvar (\n\tthisSvc     *WorkflowService\n\tthisSvcOnce sync.Once\n)\n\nfunc thisSvcInst() *WorkflowService {\n\tthisSvcOnce.Do(func() {\n\t\tthisSvc = NewWorkflowService(repository.NewWorkflowRepository(), repository.NewWorkflowRunRepository(), repository.NewSettingsRepository())\n\t})\n\treturn thisSvc\n}\n"
  },
  {
    "path": "internal/workflow/workflow.go",
    "content": "﻿package workflow\n\nimport (\n\t\"context\"\n)\n\nfunc Setup() {\n\tregisterWorkflowRecordEvents()\n}\n\nfunc Teardown() {\n\tthisSvcInst().Shutdown(context.Background())\n}\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"log/slog\"\n\t\"os\"\n\t\"strings\"\n\t_ \"time/tzdata\"\n\n\t\"github.com/pocketbase/pocketbase\"\n\t\"github.com/pocketbase/pocketbase/apis\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/pocketbase/plugins/migratecmd\"\n\t\"github.com/pocketbase/pocketbase/tools/hook\"\n\t\"github.com/spf13/pflag\"\n\n\t\"github.com/certimate-go/certimate/cmd\"\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/internal/rest/routes\"\n\t\"github.com/certimate-go/certimate/internal/scheduler\"\n\t\"github.com/certimate-go/certimate/internal/workflow\"\n\t\"github.com/certimate-go/certimate/ui\"\n\n\t_ \"github.com/certimate-go/certimate/migrations\"\n)\n\nfunc main() {\n\tpb := app.GetApp().(*pocketbase.PocketBase)\n\tif len(os.Args) < 2 {\n\t\tslog.Error(\"[CERTIMATE] missing exec args, maybe you forget the 'serve' command?\")\n\t\tos.Exit(1)\n\t\treturn\n\t}\n\n\tvar flagHttp string\n\tpflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ContinueOnError)\n\tpflag.CommandLine.Parse(os.Args[2:]) // skip the first two arguments: \"main.go serve\"\n\tpflag.StringVar(&flagHttp, \"http\", \"127.0.0.1:8090\", \"HTTP server address\")\n\tpflag.Parse()\n\n\tmigratecmd.MustRegister(pb, pb.RootCmd, migratecmd.Config{\n\t\t// enable auto creation of migration files when making collection changes in the Admin UI\n\t\t// (the isGoRun check is to enable it only during development)\n\t\tAutomigrate: strings.HasPrefix(os.Args[0], os.TempDir()),\n\t})\n\n\tpb.RootCmd.AddCommand(cmd.NewInternalCommand(pb))\n\tpb.RootCmd.AddCommand(cmd.NewWinscCommand(pb))\n\n\tpb.OnServe().BindFunc(func(e *core.ServeEvent) error {\n\t\tscheduler.Setup()\n\t\tworkflow.Setup()\n\t\troutes.BindRouter(e.Router)\n\t\treturn e.Next()\n\t})\n\n\tpb.OnServe().Bind(&hook.Handler[*core.ServeEvent]{\n\t\tFunc: func(e *core.ServeEvent) error {\n\t\t\te.Router.\n\t\t\t\tGET(\"/{path...}\", apis.Static(ui.DistDirFS, false)).\n\t\t\t\tBind(apis.Gzip())\n\t\t\treturn e.Next()\n\t\t},\n\t\tPriority: 999,\n\t})\n\n\tpb.OnServe().BindFunc(func(e *core.ServeEvent) error {\n\t\tslog.Info(\"[CERTIMATE] Visit the website: http://\" + flagHttp)\n\t\treturn e.Next()\n\t})\n\n\tpb.OnTerminate().BindFunc(func(e *core.TerminateEvent) error {\n\t\tworkflow.Teardown()\n\t\treturn e.Next()\n\t})\n\n\tif err := cmd.Serve(pb); err != nil {\n\t\tslog.Error(\"[CERTIMATE] Start failed.\", slog.Any(\"error\", err))\n\t}\n}\n"
  },
  {
    "path": "migrations/1757476800_upgrade_v0.4.0.go",
    "content": "package migrations\n\nimport (\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\tm \"github.com/pocketbase/pocketbase/migrations\"\n\t\"github.com/samber/lo\"\n\n\tsnapsv03 \"github.com/certimate-go/certimate/migrations/snaps/v0.3\"\n\tsnapsv04 \"github.com/certimate-go/certimate/migrations/snaps/v0.4\"\n)\n\nfunc init() {\n\tm.Register(func(app core.App) error {\n\t\tif err := app.DB().\n\t\t\tNewQuery(\"SELECT (1) FROM _migrations WHERE file={:file} LIMIT 1\").\n\t\t\tBind(dbx.Params{\"file\": \"1757476800_m0.4.0_migrate.go\"}).\n\t\t\tOne(&struct{}{}); err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\ttracer := NewTracer(\"v0.4.0\")\n\t\ttracer.Printf(\"go ...\")\n\n\t\t// update collection `settings`\n\t\t//   - delete records: 'notifyChannels', 'notifyTemplates'\n\t\t{\n\t\t\tcollection, err := app.FindCollectionByNameOrId(\"dy6ccjb60spfy6p\")\n\t\t\tif err != nil {\n\t\t\t\tif !errors.Is(err, sql.ErrNoRows) {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif _, err := app.DB().NewQuery(\"DELETE FROM settings WHERE name = 'notifyChannels'\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif _, err := app.DB().NewQuery(\"DELETE FROM settings WHERE name = 'notifyTemplates'\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\ttracer.Printf(\"collection '%s' updated\", collection.Name)\n\t\t\t}\n\t\t}\n\n\t\t// update collection `acme_accounts`\n\t\t//   - add field `acmeAcctUrl`\n\t\t//   - add field `acmeDirUrl`\n\t\t//   - rename field `key` to `privateKey`\n\t\t//   - rename field `resource` to `acmeAccount`\n\t\t//   - migrate field `acmeAccount`\n\t\t{\n\t\t\tcollection, err := app.FindCollectionByNameOrId(\"012d7abbod1hwvr\")\n\t\t\tif err != nil {\n\t\t\t\tif !errors.Is(err, sql.ErrNoRows) {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(5, []byte(`{\n\t\t\t\t\t\"exceptDomains\": null,\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"url2424532088\",\n\t\t\t\t\t\"name\": \"acmeAcctUrl\",\n\t\t\t\t\t\"onlyDomains\": null,\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"url\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{\n\t\t\t\t\t\"exceptDomains\": null,\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"url3632694140\",\n\t\t\t\t\t\"name\": \"acmeDirUrl\",\n\t\t\t\t\t\"onlyDomains\": null,\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"url\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{\n\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"genxqtii\",\n\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\"name\": \"privateKey\",\n\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(4, []byte(`{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"1aoia909\",\n\t\t\t\t\t\"maxSize\": 2000000,\n\t\t\t\t\t\"name\": \"acmeAccount\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"json\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := app.Save(collection); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfor _, record := range records {\n\t\t\t\t\tchanged := false\n\t\t\t\t\tdeleted := false\n\n\t\t\t\t\tresource := make(map[string]any)\n\t\t\t\t\tif err := record.UnmarshalJSONField(\"acmeAccount\", &resource); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tif _, ok := resource[\"body\"]; ok {\n\t\t\t\t\t\trecord.Set(\"acmeAcctUrl\", resource[\"uri\"].(string))\n\t\t\t\t\t\trecord.Set(\"acmeAccount\", resource[\"body\"].(map[string]any))\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\n\t\t\t\t\tca := record.GetString(\"ca\")\n\t\t\t\t\tif strings.Contains(ca, \"#\") {\n\t\t\t\t\t\trecord.Set(\"ca\", strings.Split(ca, \"#\")[0])\n\t\t\t\t\t\tif access, err := app.FindRecordById(\"access\", strings.Split(ca, \"#\")[1]); err != nil {\n\t\t\t\t\t\t\tdeleted = true\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tprovider := access.GetString(\"provider\")\n\t\t\t\t\t\t\tswitch provider {\n\t\t\t\t\t\t\tcase \"buypass\":\n\t\t\t\t\t\t\t\trecord.Set(\"acmeDirUrl\", \"https://api.buypass.com/acme/directory\")\n\t\t\t\t\t\t\t\tchanged = true\n\n\t\t\t\t\t\t\tcase \"googletrustservices\":\n\t\t\t\t\t\t\t\trecord.Set(\"acmeDirUrl\", \"https://dv.acme-v02.api.pki.goog/directory\")\n\t\t\t\t\t\t\t\tchanged = true\n\n\t\t\t\t\t\t\tcase \"sslcom\":\n\t\t\t\t\t\t\t\trecord.Set(\"acmeDirUrl\", \"https://acme.ssl.com/sslcom-dv-rsa\")\n\t\t\t\t\t\t\t\tchanged = true\n\n\t\t\t\t\t\t\tcase \"zerossl\":\n\t\t\t\t\t\t\t\trecord.Set(\"acmeDirUrl\", \"https://acme.zerossl.com/v2/DV90\")\n\t\t\t\t\t\t\t\tchanged = true\n\n\t\t\t\t\t\t\tcase \"acmeca\":\n\t\t\t\t\t\t\t\taccessConfig := make(map[string]any)\n\t\t\t\t\t\t\t\taccess.UnmarshalJSONField(\"config\", &accessConfig)\n\t\t\t\t\t\t\t\trecord.Set(\"acmeDirUrl\", accessConfig[\"endpoint\"].(string))\n\t\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tswitch ca {\n\t\t\t\t\t\tcase \"letsencrypt\":\n\t\t\t\t\t\t\trecord.Set(\"acmeDirUrl\", \"https://acme-v02.api.letsencrypt.org/directory\")\n\t\t\t\t\t\t\tchanged = true\n\n\t\t\t\t\t\tcase \"letsencryptstaging\":\n\t\t\t\t\t\t\trecord.Set(\"acmeDirUrl\", \"https://acme-staging-v02.api.letsencrypt.org/directory\")\n\t\t\t\t\t\t\tchanged = true\n\n\t\t\t\t\t\tcase \"buypass\":\n\t\t\t\t\t\t\trecord.Set(\"acmeDirUrl\", \"https://api.buypass.com/acme/directory\")\n\t\t\t\t\t\t\tchanged = true\n\n\t\t\t\t\t\tcase \"googletrustservices\":\n\t\t\t\t\t\t\trecord.Set(\"acmeDirUrl\", \"https://dv.acme-v02.api.pki.goog/directory\")\n\t\t\t\t\t\t\tchanged = true\n\n\t\t\t\t\t\tcase \"sslcom\":\n\t\t\t\t\t\t\trecord.Set(\"acmeDirUrl\", \"https://acme.ssl.com/sslcom-dv-rsa\")\n\t\t\t\t\t\t\tchanged = true\n\n\t\t\t\t\t\tcase \"zerossl\":\n\t\t\t\t\t\t\trecord.Set(\"acmeDirUrl\", \"https://acme.zerossl.com/v2/DV90\")\n\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif changed {\n\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t}\n\n\t\t\t\t\tif deleted {\n\t\t\t\t\t\tif err := app.Delete(record); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' deleted\", record.Id, collection.Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\ttracer.Printf(\"collection '%s' updated\", collection.Name)\n\t\t\t}\n\t\t}\n\n\t\t// update collection `access`\n\t\t//   - modify field `config` schema: rename property `defaultReceiver` to `receiver`\n\t\t//   - modify field `reserve` candidates\n\t\t//   - delete records: 'local', 'buypass'\n\t\t{\n\t\t\tcollection, err := app.FindCollectionByNameOrId(\"4yzbv8urny5ja1e\")\n\t\t\tif err != nil {\n\t\t\t\tif !errors.Is(err, sql.ErrNoRows) {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE access SET reserve = 'notif' WHERE reserve = 'notification'\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif _, err := app.DB().NewQuery(\"DELETE FROM access WHERE provider = 'local'\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif _, err := app.DB().NewQuery(\"DELETE FROM access WHERE provider = 'buypass'\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfor _, record := range records {\n\t\t\t\t\tchanged := false\n\n\t\t\t\t\tprovider := record.GetString(\"provider\")\n\t\t\t\t\tconfig := make(map[string]any)\n\t\t\t\t\tif err := record.UnmarshalJSONField(\"config\", &config); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tswitch provider {\n\t\t\t\t\tcase \"discordbot\", \"mattermost\", \"slackbot\":\n\t\t\t\t\t\tif _, ok := config[\"defaultChannelId\"]; ok {\n\t\t\t\t\t\t\tconfig[\"channelId\"] = config[\"defaultChannelId\"]\n\t\t\t\t\t\t\tdelete(config, \"defaultChannelId\")\n\t\t\t\t\t\t\trecord.Set(\"config\", config)\n\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t}\n\n\t\t\t\t\tcase \"email\":\n\t\t\t\t\t\tif _, ok := config[\"defaultSenderAddress\"]; ok {\n\t\t\t\t\t\t\tconfig[\"senderAddress\"] = config[\"defaultSenderAddress\"]\n\t\t\t\t\t\t\tdelete(config, \"defaultSenderAddress\")\n\t\t\t\t\t\t\trecord.Set(\"config\", config)\n\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif _, ok := config[\"defaultSenderName\"]; ok {\n\t\t\t\t\t\t\tconfig[\"senderName\"] = config[\"defaultSenderName\"]\n\t\t\t\t\t\t\tdelete(config, \"defaultSenderName\")\n\t\t\t\t\t\t\trecord.Set(\"config\", config)\n\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif _, ok := config[\"defaultReceiverAddress\"]; ok {\n\t\t\t\t\t\t\tconfig[\"receiverAddress\"] = config[\"defaultReceiverAddress\"]\n\t\t\t\t\t\t\tdelete(config, \"defaultReceiverAddress\")\n\t\t\t\t\t\t\trecord.Set(\"config\", config)\n\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t}\n\n\t\t\t\t\tcase \"telegrambot\":\n\t\t\t\t\t\tif _, ok := config[\"defaultChatId\"]; ok {\n\t\t\t\t\t\t\tconfig[\"chatId\"] = config[\"defaultChatId\"]\n\t\t\t\t\t\t\tdelete(config, \"defaultChatId\")\n\t\t\t\t\t\t\trecord.Set(\"config\", config)\n\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t}\n\n\t\t\t\t\tcase \"webhook\":\n\t\t\t\t\t\tif _, ok := config[\"defaultDataForDeployment\"]; ok {\n\t\t\t\t\t\t\tif existsData, exists := config[\"data\"]; !exists || existsData == \"\" {\n\t\t\t\t\t\t\t\tconfig[\"data\"] = config[\"defaultDataForDeployment\"]\n\t\t\t\t\t\t\t\tdelete(config, \"defaultDataForDeployment\")\n\t\t\t\t\t\t\t\trecord.Set(\"config\", config)\n\t\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif _, ok := config[\"defaultDataForNotification\"]; ok {\n\t\t\t\t\t\t\tif existsData, exists := config[\"data\"]; !exists || existsData == \"\" {\n\t\t\t\t\t\t\t\tconfig[\"data\"] = config[\"defaultDataForNotification\"]\n\t\t\t\t\t\t\t\tdelete(config, \"defaultDataForNotification\")\n\t\t\t\t\t\t\t\trecord.Set(\"config\", config)\n\t\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif _, ok := config[\"dataForDeployment\"]; ok {\n\t\t\t\t\t\t\tif existsData, exists := config[\"data\"]; !exists || existsData == \"\" {\n\t\t\t\t\t\t\t\tconfig[\"data\"] = config[\"dataForDeployment\"]\n\t\t\t\t\t\t\t\tdelete(config, \"dataForDeployment\")\n\t\t\t\t\t\t\t\trecord.Set(\"config\", config)\n\t\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif _, ok := config[\"dataForNotification\"]; ok {\n\t\t\t\t\t\t\tif existsData, exists := config[\"data\"]; !exists || existsData == \"\" {\n\t\t\t\t\t\t\t\tconfig[\"data\"] = config[\"dataForNotification\"]\n\t\t\t\t\t\t\t\tdelete(config, \"dataForNotification\")\n\t\t\t\t\t\t\t\trecord.Set(\"config\", config)\n\t\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif changed {\n\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// update collection `certificate`\n\t\t//   - modify field `source` candidates\n\t\t//   - rename field `effectAt` to `validityNotBefore`\n\t\t//   - rename field `expireAt` to `validityNotAfter`\n\t\t//   - rename field `acmeAccountUrl` to `acmeAcctUrl`\n\t\t//   - rename field `workflowId` to `workflowRef`\n\t\t//   - rename field `workflowRunId` to `workflowRunRef`\n\t\t//   - rename field `workflowOutputId`(aka `workflowOutputRef`)\n\t\t{\n\t\t\tcollection, err := app.FindCollectionByNameOrId(\"4szxr9x43tpj6np\")\n\t\t\tif err != nil {\n\t\t\t\tif !errors.Is(err, sql.ErrNoRows) {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"by9hetqi\",\n\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\"name\": \"source\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"select\",\n\t\t\t\t\t\"values\": [\n\t\t\t\t\t\t\"request\",\n\t\t\t\t\t\t\"upload\"\n\t\t\t\t\t]\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"v40aqzpd\",\n\t\t\t\t\t\"max\": \"\",\n\t\t\t\t\t\"min\": \"\",\n\t\t\t\t\t\"name\": \"validityNotBefore\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"date\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(10, []byte(`{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"zgpdby2k\",\n\t\t\t\t\t\"max\": \"\",\n\t\t\t\t\t\"min\": \"\",\n\t\t\t\t\t\"name\": \"validityNotAfter\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"date\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(11, []byte(`{\n\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"text2045248758\",\n\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\"name\": \"acmeAcctUrl\",\n\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(15, []byte(`{\n\t\t\t\t\t\"cascadeDelete\": false,\n\t\t\t\t\t\"collectionId\": \"tovyif5ax6j62ur\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"uvqfamb1\",\n\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\"name\": \"workflowRef\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(16, []byte(`{\n\t\t\t\t\t\"cascadeDelete\": false,\n\t\t\t\t\t\"collectionId\": \"qjp8lygssgwyqyz\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"relation3917999135\",\n\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\"name\": \"workflowRunRef\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tcollection.Fields.RemoveByName(\"workflowOutputId\")\n\t\t\t\tcollection.Fields.RemoveByName(\"workflowOutputRef\")\n\n\t\t\t\tif err := json.Unmarshal([]byte(`{\n\t\t\t\t\t\"indexes\": [\n\t\t\t\t\t\t\"CREATE INDEX `+\"`\"+`idx_Jx8TXzDCmw`+\"`\"+` ON `+\"`\"+`certificate`+\"`\"+` (`+\"`\"+`workflowRef`+\"`\"+`)\",\n\t\t\t\t\t\t\"CREATE INDEX `+\"`\"+`idx_2cRXqNDyyp`+\"`\"+` ON `+\"`\"+`certificate`+\"`\"+` (`+\"`\"+`workflowRunRef`+\"`\"+`)\",\n\t\t\t\t\t\t\"CREATE INDEX `+\"`\"+`idx_kcKpgAZapk`+\"`\"+` ON `+\"`\"+`certificate`+\"`\"+` (`+\"`\"+`workflowNodeId`+\"`\"+`)\"\n\t\t\t\t\t]\n\t\t\t\t}`), &collection); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := app.Save(collection); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE certificate SET source = 'request' WHERE source = 'workflow'\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\ttracer.Printf(\"collection '%s' updated\", collection.Name)\n\t\t\t}\n\t\t}\n\n\t\t// update collection `workflow`\n\t\t//   - modify field `trigger` candidates, and cascading migrate field `graphDraft` / `graphContent`\n\t\t//   - modify field `lastRunStatus` candidates\n\t\t//   - rename field `lastRunRefId` to `lastRunRef`\n\t\t//   - rename field `draft` to `graphDraft`\n\t\t//   - rename field `content` to `graphContent`\n\t\t//   - add field `hasContent`\n\t\t{\n\t\t\tcollection, err := app.FindCollectionByNameOrId(\"tovyif5ax6j62ur\")\n\t\t\tif err != nil {\n\t\t\t\tif !errors.Is(err, sql.ErrNoRows) {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"vqoajwjq\",\n\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\"name\": \"trigger\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"select\",\n\t\t\t\t\t\"values\": [\n\t\t\t\t\t\t\"manual\",\n\t\t\t\t\t\t\"scheduled\"\n\t\t\t\t\t]\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"g9ohkk5o\",\n\t\t\t\t\t\"maxSize\": 5000000,\n\t\t\t\t\t\"name\": \"graphDraft\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"json\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(7, []byte(`{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"awlphkfe\",\n\t\t\t\t\t\"maxSize\": 5000000,\n\t\t\t\t\t\"name\": \"graphContent\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"json\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"bool3832150317\",\n\t\t\t\t\t\"name\": \"hasContent\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"bool\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(10, []byte(`{\n\t\t\t\t\t\"cascadeDelete\": false,\n\t\t\t\t\t\"collectionId\": \"qjp8lygssgwyqyz\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"a23wkj9x\",\n\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\"name\": \"lastRunRef\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(11, []byte(`{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"zivdxh23\",\n\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\"name\": \"lastRunStatus\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"select\",\n\t\t\t\t\t\"values\": [\n\t\t\t\t\t\t\"pending\",\n\t\t\t\t\t\t\"processing\",\n\t\t\t\t\t\t\"succeeded\",\n\t\t\t\t\t\t\"failed\",\n\t\t\t\t\t\t\"canceled\"\n\t\t\t\t\t]\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := app.Save(collection); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow SET trigger = 'scheduled' WHERE trigger = 'auto'\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow SET hasContent = TRUE WHERE graphContent IS NOT NULL\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow SET lastRunStatus = 'processing' WHERE lastRunStatus = 'running'\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\ttracer.Printf(\"collection '%s' updated\", collection.Name)\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t} else {\n\t\t\t\t\tfor _, record := range records {\n\t\t\t\t\t\tchanged := false\n\n\t\t\t\t\t\tgraphDraft := make(map[string]any)\n\t\t\t\t\t\tif err := record.UnmarshalJSONField(\"graphDraft\", &graphDraft); err == nil {\n\t\t\t\t\t\t\tif _, ok := graphDraft[\"config\"]; ok {\n\t\t\t\t\t\t\t\tconfig := graphDraft[\"config\"].(map[string]any)\n\t\t\t\t\t\t\t\tif _, ok := config[\"trigger\"]; ok {\n\t\t\t\t\t\t\t\t\ttrigger := config[\"trigger\"].(string)\n\t\t\t\t\t\t\t\t\tif trigger == \"auto\" {\n\t\t\t\t\t\t\t\t\t\tconfig[\"trigger\"] = \"scheduled\"\n\t\t\t\t\t\t\t\t\t\trecord.Set(\"graphDraft\", graphDraft)\n\t\t\t\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tgraphContent := make(map[string]any)\n\t\t\t\t\t\tif err := record.UnmarshalJSONField(\"graphContent\", &graphContent); err == nil {\n\t\t\t\t\t\t\tif _, ok := graphContent[\"config\"]; ok {\n\t\t\t\t\t\t\t\tconfig := graphContent[\"config\"].(map[string]any)\n\t\t\t\t\t\t\t\tif _, ok := config[\"trigger\"]; ok {\n\t\t\t\t\t\t\t\t\ttrigger := config[\"trigger\"].(string)\n\t\t\t\t\t\t\t\t\tif trigger == \"auto\" {\n\t\t\t\t\t\t\t\t\t\tconfig[\"trigger\"] = \"scheduled\"\n\t\t\t\t\t\t\t\t\t\trecord.Set(\"graphContent\", graphContent)\n\t\t\t\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif changed {\n\t\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// update collection `workflow_run`\n\t\t//   - modify field `trigger` candidates, and cascading migrate field `graph`\n\t\t//   - modify field `status` candidates\n\t\t//   - rename field `detail` to `graph`\n\t\t//   - rename field `workflowId` to `workflowRef`\n\t\t{\n\t\t\tcollection, err := app.FindCollectionByNameOrId(\"qjp8lygssgwyqyz\")\n\t\t\tif err != nil {\n\t\t\t\tif !errors.Is(err, sql.ErrNoRows) {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{\n\t\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\t\"collectionId\": \"tovyif5ax6j62ur\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"m8xfsyyy\",\n\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\"name\": \"workflowRef\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"qldmh0tw\",\n\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\"name\": \"status\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"select\",\n\t\t\t\t\t\"values\": [\n\t\t\t\t\t\t\"pending\",\n\t\t\t\t\t\t\"processing\",\n\t\t\t\t\t\t\"succeeded\",\n\t\t\t\t\t\t\"failed\",\n\t\t\t\t\t\t\"canceled\"\n\t\t\t\t\t]\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"jlroa3fk\",\n\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\"name\": \"trigger\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"select\",\n\t\t\t\t\t\"values\": [\n\t\t\t\t\t\t\"manual\",\n\t\t\t\t\t\t\"scheduled\"\n\t\t\t\t\t]\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"json772177811\",\n\t\t\t\t\t\"maxSize\": 5000000,\n\t\t\t\t\t\"name\": \"graph\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"json\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := json.Unmarshal([]byte(`{\n\t\t\t\t\t\"indexes\": [\n\t\t\t\t\t\t\"CREATE INDEX `+\"`\"+`idx_7ZpfjTFsD2`+\"`\"+` ON `+\"`\"+`workflow_run`+\"`\"+` (`+\"`\"+`workflowRef`+\"`\"+`)\"\n\t\t\t\t\t]\n\t\t\t\t}`), &collection); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := app.Save(collection); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow_run SET trigger = 'scheduled' WHERE trigger = 'auto'\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow_run SET status = 'processing' WHERE status = 'running'\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\ttracer.Printf(\"collection '%s' updated\", collection.Name)\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t} else {\n\t\t\t\t\tfor _, record := range records {\n\t\t\t\t\t\tchanged := false\n\n\t\t\t\t\t\tgraphContent := make(map[string]any)\n\t\t\t\t\t\tif err := record.UnmarshalJSONField(\"graph\", &graphContent); err == nil {\n\t\t\t\t\t\t\tif _, ok := graphContent[\"config\"]; ok {\n\t\t\t\t\t\t\t\tconfig := graphContent[\"config\"].(map[string]any)\n\t\t\t\t\t\t\t\tif _, ok := config[\"trigger\"]; ok {\n\t\t\t\t\t\t\t\t\ttrigger := config[\"trigger\"].(string)\n\t\t\t\t\t\t\t\t\tif trigger == \"auto\" {\n\t\t\t\t\t\t\t\t\t\tconfig[\"trigger\"] = \"scheduled\"\n\t\t\t\t\t\t\t\t\t\trecord.Set(\"graph\", graphContent)\n\t\t\t\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif changed {\n\t\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// update collection `workflow_output`\n\t\t//   - rename field `workflowId` to `workflowRef`\n\t\t//   - rename field `runId` to `runRef`\n\t\t//   - rename field `node` to `nodeConfig`\n\t\t{\n\t\t\tcollection, err := app.FindCollectionByNameOrId(\"bqnxb95f2cooowp\")\n\t\t\tif err != nil {\n\t\t\t\tif !errors.Is(err, sql.ErrNoRows) {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err := json.Unmarshal([]byte(`{\n\t\t\t\t\t\"indexes\": [\n\t\t\t\t\t\t\"CREATE INDEX `+\"`\"+`idx_BYoQPsz4my`+\"`\"+` ON `+\"`\"+`workflow_output`+\"`\"+` (`+\"`\"+`workflowRef`+\"`\"+`)\",\n\t\t\t\t\t\t\"CREATE INDEX `+\"`\"+`idx_O9zxLETuxJ`+\"`\"+` ON `+\"`\"+`workflow_output`+\"`\"+` (`+\"`\"+`runRef`+\"`\"+`)\",\n\t\t\t\t\t\t\"CREATE INDEX `+\"`\"+`idx_luac8Ul34G`+\"`\"+` ON `+\"`\"+`workflow_output`+\"`\"+` (`+\"`\"+`nodeId`+\"`\"+`)\"\n\t\t\t\t\t]\n\t\t\t\t}`), &collection); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{\n\t\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\t\"collectionId\": \"tovyif5ax6j62ur\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"jka88auc\",\n\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\"name\": \"workflowRef\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{\n\t\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\t\"collectionId\": \"qjp8lygssgwyqyz\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"relation821863227\",\n\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\"name\": \"runRef\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(4, []byte(`{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"json2239752261\",\n\t\t\t\t\t\"maxSize\": 5000000,\n\t\t\t\t\t\"name\": \"nodeConfig\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"json\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := app.Save(collection); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\ttracer.Printf(\"collection '%s' updated\", collection.Name)\n\t\t\t}\n\t\t}\n\n\t\t// update collection `workflow_logs`\n\t\t//   - modify field `level` type\n\t\t//   - rename field `workflowId` to `workflowRef`\n\t\t//   - rename field `runId` to `runRef`\n\t\t//   - migrate field `message`\n\t\t{\n\t\t\tcollection, err := app.FindCollectionByNameOrId(\"pbc_1682296116\")\n\t\t\tif err != nil {\n\t\t\t\tif !errors.Is(err, sql.ErrNoRows) {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif field := collection.Fields.GetByName(\"level\"); field != nil && field.Type() == \"text\" {\n\t\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow_logs SET level = '-4' WHERE level = 'DEBUG'\").Execute(); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow_logs SET level = '0' WHERE level = 'INFO'\").Execute(); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow_logs SET level = '4' WHERE level = 'WARN'\").Execute(); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow_logs SET level = '8' WHERE level = 'ERROR'\").Execute(); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(7, []byte(`{\n\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\"id\": \"number760395071\",\n\t\t\t\t\t\t\"max\": null,\n\t\t\t\t\t\t\"min\": null,\n\t\t\t\t\t\t\"name\": \"levelTmp\",\n\t\t\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\"type\": \"number\"\n\t\t\t\t\t}`)); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tif err := app.Save(collection); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow_logs SET levelTmp = level\").Execute(); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tcollection.Fields.RemoveById(field.GetId())\n\t\t\t\t\tif err := app.Save(collection); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{\n\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\"id\": \"number760395071\",\n\t\t\t\t\t\t\"max\": null,\n\t\t\t\t\t\t\"min\": null,\n\t\t\t\t\t\t\"name\": \"level\",\n\t\t\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\"type\": \"number\"\n\t\t\t\t\t}`)); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tif err := app.Save(collection); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{\n\t\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\t\"collectionId\": \"tovyif5ax6j62ur\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"relation3371272342\",\n\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\"name\": \"workflowRef\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{\n\t\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\t\"collectionId\": \"qjp8lygssgwyqyz\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"relation821863227\",\n\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\"name\": \"runRef\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t}`)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := json.Unmarshal([]byte(`{\n\t\t\t\t\t\"indexes\": [\n\t\t\t\t\t\t\"CREATE INDEX `+\"`\"+`idx_IOlpy6XuJ2`+\"`\"+` ON `+\"`\"+`workflow_logs`+\"`\"+` (`+\"`\"+`workflowRef`+\"`\"+`)\",\n\t\t\t\t\t\t\"CREATE INDEX `+\"`\"+`idx_qVlTb2yl7v`+\"`\"+` ON `+\"`\"+`workflow_logs`+\"`\"+` (`+\"`\"+`runRef`+\"`\"+`)\",\n\t\t\t\t\t\t\"CREATE INDEX `+\"`\"+`idx_UL4tdCXNlA`+\"`\"+` ON `+\"`\"+`workflow_logs`+\"`\"+` (`+\"`\"+`nodeId`+\"`\"+`)\"\n\t\t\t\t\t]\n\t\t\t\t}`), &collection); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow_logs SET message = REPLACE(message, 'certificiate', 'certificate') WHERE level = 0\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow_logs SET message = REPLACE(message, 'ready to apply certificate', 'ready to request certificate') WHERE level = 0\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow_logs SET message = REPLACE(message, 'ready to obtain certificate', 'ready to request certificate') WHERE level = 0\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif err := app.Save(collection); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\ttracer.Printf(\"collection '%s' updated\", collection.Name)\n\t\t\t}\n\t\t}\n\n\t\t// adapt to new workflow data structure\n\t\t{\n\t\t\tconvertNode := func(root *snapsv03.WorkflowNode) []*snapsv04.WorkflowNode {\n\t\t\t\tlang := lo.\n\t\t\t\t\tIfF(root == nil, func() string { return \"zh\" }).\n\t\t\t\t\tElseIf(regexp.MustCompile(`[\\p{Han}]`).MatchString(root.Name), \"zh\").\n\t\t\t\t\tElse(\"en\")\n\n\t\t\t\tvar deepConvertNode func(node *snapsv03.WorkflowNode) []*snapsv04.WorkflowNode\n\t\t\t\tdeepConvertNode = func(node *snapsv03.WorkflowNode) []*snapsv04.WorkflowNode {\n\t\t\t\t\ttemp := make([]*snapsv04.WorkflowNode, 0)\n\n\t\t\t\t\tcurrent := node\n\t\t\t\t\tfor current != nil {\n\t\t\t\t\t\tcurrent.Config = lo.PickBy(current.Config, func(key string, value any) bool {\n\t\t\t\t\t\t\tstr, ok := value.(string)\n\t\t\t\t\t\t\treturn !ok || str != \"\"\n\t\t\t\t\t\t})\n\n\t\t\t\t\t\tswitch current.Type {\n\t\t\t\t\t\tcase \"start\":\n\t\t\t\t\t\t\ttemp = append(temp, &snapsv04.WorkflowNode{\n\t\t\t\t\t\t\t\tId:   current.Id,\n\t\t\t\t\t\t\t\tType: \"start\",\n\t\t\t\t\t\t\t\tData: snapsv04.WorkflowNodeData{\n\t\t\t\t\t\t\t\t\tName:   current.Name,\n\t\t\t\t\t\t\t\t\tConfig: current.Config,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t})\n\n\t\t\t\t\t\tcase \"apply\":\n\t\t\t\t\t\t\tif _, ok := current.Config[\"challengeType\"].(string); !ok {\n\t\t\t\t\t\t\t\tcurrent.Config[\"challengeType\"] = \"dns-01\"\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\ttemp = append(temp, &snapsv04.WorkflowNode{\n\t\t\t\t\t\t\t\tId:   current.Id,\n\t\t\t\t\t\t\t\tType: \"bizApply\",\n\t\t\t\t\t\t\t\tData: snapsv04.WorkflowNodeData{\n\t\t\t\t\t\t\t\t\tName:   current.Name,\n\t\t\t\t\t\t\t\t\tConfig: current.Config,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t})\n\n\t\t\t\t\t\tcase \"upload\":\n\t\t\t\t\t\t\tif _, ok := current.Config[\"source\"].(string); !ok {\n\t\t\t\t\t\t\t\tcurrent.Config[\"source\"] = \"form\"\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\ttemp = append(temp, &snapsv04.WorkflowNode{\n\t\t\t\t\t\t\t\tId:   current.Id,\n\t\t\t\t\t\t\t\tType: \"bizUpload\",\n\t\t\t\t\t\t\t\tData: snapsv04.WorkflowNodeData{\n\t\t\t\t\t\t\t\t\tName:   current.Name,\n\t\t\t\t\t\t\t\t\tConfig: current.Config,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t})\n\n\t\t\t\t\t\tcase \"monitor\":\n\t\t\t\t\t\t\ttemp = append(temp, &snapsv04.WorkflowNode{\n\t\t\t\t\t\t\t\tId:   current.Id,\n\t\t\t\t\t\t\t\tType: \"bizMonitor\",\n\t\t\t\t\t\t\t\tData: snapsv04.WorkflowNodeData{\n\t\t\t\t\t\t\t\t\tName:   current.Name,\n\t\t\t\t\t\t\t\t\tConfig: current.Config,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t})\n\n\t\t\t\t\t\tcase \"deploy\":\n\t\t\t\t\t\t\tif s, ok := current.Config[\"certificate\"].(string); ok {\n\t\t\t\t\t\t\t\tcurrent.Config[\"certificateOutputNodeId\"] = strings.Split(s, \"#\")[0]\n\t\t\t\t\t\t\t\tdelete(current.Config, \"certificate\")\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\ttemp = append(temp, &snapsv04.WorkflowNode{\n\t\t\t\t\t\t\t\tId:   current.Id,\n\t\t\t\t\t\t\t\tType: \"bizDeploy\",\n\t\t\t\t\t\t\t\tData: snapsv04.WorkflowNodeData{\n\t\t\t\t\t\t\t\t\tName:   current.Name,\n\t\t\t\t\t\t\t\t\tConfig: current.Config,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t})\n\n\t\t\t\t\t\tcase \"notify\":\n\t\t\t\t\t\t\tif _, ok := current.Config[\"channel\"].(string); ok {\n\t\t\t\t\t\t\t\tdelete(current.Config, \"channel\")\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\ttemp = append(temp, &snapsv04.WorkflowNode{\n\t\t\t\t\t\t\t\tId:   current.Id,\n\t\t\t\t\t\t\t\tType: \"bizNotify\",\n\t\t\t\t\t\t\t\tData: snapsv04.WorkflowNodeData{\n\t\t\t\t\t\t\t\t\tName:   current.Name,\n\t\t\t\t\t\t\t\t\tConfig: current.Config,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t})\n\n\t\t\t\t\t\tcase \"execute_result_branch\":\n\t\t\t\t\t\t\tif len(temp) == 0 {\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\ttryNode, _ := lo.Last(temp)\n\t\t\t\t\t\t\ttemp = lo.DropRight(temp, 1)\n\n\t\t\t\t\t\t\tbranches := lo.GroupBy(current.Branches, func(b *snapsv03.WorkflowNode) string { return b.Type })\n\t\t\t\t\t\t\tsuccessBranch := lo.IfF(len(branches[\"execute_success\"]) > 0, func() *snapsv03.WorkflowNode {\n\t\t\t\t\t\t\t\treturn branches[\"execute_success\"][0]\n\t\t\t\t\t\t\t}).Else(nil)\n\t\t\t\t\t\t\tfailureBranch := lo.IfF(len(branches[\"execute_failure\"]) > 0, func() *snapsv03.WorkflowNode {\n\t\t\t\t\t\t\t\treturn branches[\"execute_failure\"][0]\n\t\t\t\t\t\t\t}).Else(nil)\n\t\t\t\t\t\t\tsuccessBranchId := lo.If(successBranch != nil, successBranch.Id).Else(core.GenerateDefaultRandomId())\n\t\t\t\t\t\t\tfailureBranchId := lo.If(failureBranch != nil, failureBranch.Id).Else(core.GenerateDefaultRandomId())\n\n\t\t\t\t\t\t\tcatchBlocks := lo.If(failureBranch != nil && failureBranch.Next != nil, deepConvertNode(failureBranch.Next)).Else([]*snapsv04.WorkflowNode{})\n\t\t\t\t\t\t\tcatchBlocks = append(catchBlocks, &snapsv04.WorkflowNode{\n\t\t\t\t\t\t\t\tId:   core.GenerateDefaultRandomId(),\n\t\t\t\t\t\t\t\tType: \"end\",\n\t\t\t\t\t\t\t\tData: snapsv04.WorkflowNodeData{\n\t\t\t\t\t\t\t\t\tName: lo.If(lang == \"en\", \"End\").Else(\"结束\"),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t})\n\n\t\t\t\t\t\t\ttryCatchNode := &snapsv04.WorkflowNode{\n\t\t\t\t\t\t\t\tId:   current.Id,\n\t\t\t\t\t\t\t\tType: \"tryCatch\",\n\t\t\t\t\t\t\t\tData: snapsv04.WorkflowNodeData{\n\t\t\t\t\t\t\t\t\tName:   lo.If(lang == \"en\", \"Try to ...\").Else(\"尝试执行…\"),\n\t\t\t\t\t\t\t\t\tConfig: current.Config,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tBlocks: []*snapsv04.WorkflowNode{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tId:   successBranchId,\n\t\t\t\t\t\t\t\t\t\tType: \"tryBlock\",\n\t\t\t\t\t\t\t\t\t\tData: snapsv04.WorkflowNodeData{\n\t\t\t\t\t\t\t\t\t\t\tName: \"\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tBlocks: []*snapsv04.WorkflowNode{tryNode},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tId:   failureBranchId,\n\t\t\t\t\t\t\t\t\t\tType: \"catchBlock\",\n\t\t\t\t\t\t\t\t\t\tData: snapsv04.WorkflowNodeData{\n\t\t\t\t\t\t\t\t\t\t\tName: lo.If(lang == \"en\", \"On failed ...\").Else(\"若执行失败…\"),\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tBlocks: catchBlocks,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\ttemp = append(temp, tryCatchNode)\n\t\t\t\t\t\t\tcurrent = successBranch\n\n\t\t\t\t\t\tcase \"branch\":\n\t\t\t\t\t\t\tbranchNode := &snapsv04.WorkflowNode{\n\t\t\t\t\t\t\t\tId:   current.Id,\n\t\t\t\t\t\t\t\tType: \"condition\",\n\t\t\t\t\t\t\t\tData: snapsv04.WorkflowNodeData{\n\t\t\t\t\t\t\t\t\tName:   lo.If(lang == \"en\", \"Parallel\").Else(\"并行\"),\n\t\t\t\t\t\t\t\t\tConfig: current.Config,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tBlocks: lo.Map(current.Branches, func(b *snapsv03.WorkflowNode, _ int) *snapsv04.WorkflowNode {\n\t\t\t\t\t\t\t\t\treturn &snapsv04.WorkflowNode{\n\t\t\t\t\t\t\t\t\t\tId:   b.Id,\n\t\t\t\t\t\t\t\t\t\tType: \"branchBlock\",\n\t\t\t\t\t\t\t\t\t\tData: snapsv04.WorkflowNodeData{\n\t\t\t\t\t\t\t\t\t\t\tName:   b.Name,\n\t\t\t\t\t\t\t\t\t\t\tConfig: b.Config,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tBlocks: deepConvertNode(b.Next),\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\ttemp = append(temp, branchNode)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif current != nil {\n\t\t\t\t\t\t\tcurrent = current.Next\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn temp\n\t\t\t\t}\n\n\t\t\t\tnodes := lo.Ternary(root == nil, []*snapsv04.WorkflowNode{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:   core.GenerateDefaultRandomId(),\n\t\t\t\t\t\tType: \"start\",\n\t\t\t\t\t\tData: snapsv04.WorkflowNodeData{\n\t\t\t\t\t\t\tName: lo.If(lang == \"en\", \"Start\").Else(\"开始\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, deepConvertNode(root))\n\n\t\t\t\treturn append(nodes, &snapsv04.WorkflowNode{\n\t\t\t\t\tId:   core.GenerateDefaultRandomId(),\n\t\t\t\t\tType: \"end\",\n\t\t\t\t\tData: snapsv04.WorkflowNodeData{\n\t\t\t\t\t\tName: lo.If(lang == \"en\", \"End\").Else(\"结束\"),\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// update collection `workflow`\n\t\t\t//   - migrate field `graphDraft` / `graphContent`\n\t\t\t{\n\t\t\t\tcollection, err := app.FindCollectionByNameOrId(\"tovyif5ax6j62ur\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tif !errors.Is(err, sql.ErrNoRows) {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfor _, record := range records {\n\t\t\t\t\t\t\tchanged := false\n\n\t\t\t\t\t\t\tgraphDraft := make(map[string]any)\n\t\t\t\t\t\t\tif err := record.UnmarshalJSONField(\"graphDraft\", &graphDraft); err == nil {\n\t\t\t\t\t\t\t\tif len(graphDraft) > 0 {\n\t\t\t\t\t\t\t\t\tif _, ok := graphDraft[\"nodes\"]; !ok {\n\t\t\t\t\t\t\t\t\t\tlegacyRootNode := &snapsv03.WorkflowNode{}\n\t\t\t\t\t\t\t\t\t\tif err := record.UnmarshalJSONField(\"graphDraft\", legacyRootNode); err != nil {\n\t\t\t\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tgraphDraft = make(map[string]any)\n\t\t\t\t\t\t\t\t\t\t\tgraphDraft[\"nodes\"] = convertNode(legacyRootNode)\n\t\t\t\t\t\t\t\t\t\t\trecord.Set(\"graphDraft\", graphDraft)\n\t\t\t\t\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tgraphContent := make(map[string]any)\n\t\t\t\t\t\t\tif err := record.UnmarshalJSONField(\"graphContent\", &graphContent); err == nil {\n\t\t\t\t\t\t\t\tif len(graphContent) > 0 {\n\t\t\t\t\t\t\t\t\tif _, ok := graphContent[\"nodes\"]; !ok {\n\t\t\t\t\t\t\t\t\t\tlegacyRootNode := &snapsv03.WorkflowNode{}\n\t\t\t\t\t\t\t\t\t\tif err := record.UnmarshalJSONField(\"graphContent\", legacyRootNode); err != nil {\n\t\t\t\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tgraphContent = make(map[string]any)\n\t\t\t\t\t\t\t\t\t\t\tgraphContent[\"nodes\"] = convertNode(legacyRootNode)\n\t\t\t\t\t\t\t\t\t\t\trecord.Set(\"graphContent\", graphContent)\n\t\t\t\t\t\t\t\t\t\t\trecord.Set(\"hasContent\", true)\n\t\t\t\t\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif changed {\n\t\t\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// update collection `workflow_run`\n\t\t\t//   - migrate field `graph`\n\t\t\t{\n\t\t\t\tcollection, err := app.FindCollectionByNameOrId(\"qjp8lygssgwyqyz\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tif !errors.Is(err, sql.ErrNoRows) {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfor _, record := range records {\n\t\t\t\t\t\t\tchanged := false\n\n\t\t\t\t\t\t\tgraphContent := make(map[string]any)\n\t\t\t\t\t\t\tif err := record.UnmarshalJSONField(\"graph\", &graphContent); err == nil {\n\t\t\t\t\t\t\t\tif len(graphContent) > 0 {\n\t\t\t\t\t\t\t\t\tif _, ok := graphContent[\"nodes\"]; !ok {\n\t\t\t\t\t\t\t\t\t\tlegacyRootNode := &snapsv03.WorkflowNode{}\n\t\t\t\t\t\t\t\t\t\tif err := record.UnmarshalJSONField(\"graph\", legacyRootNode); err != nil {\n\t\t\t\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tgraphContent = make(map[string]any)\n\t\t\t\t\t\t\t\t\t\t\tgraphContent[\"nodes\"] = convertNode(legacyRootNode)\n\t\t\t\t\t\t\t\t\t\t\trecord.Set(\"graph\", graphContent)\n\t\t\t\t\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif changed {\n\t\t\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// update collection `workflow_output`\n\t\t\t//   - migrate field `nodeConfig`\n\t\t\t//   - migrate field `outputs`\n\t\t\t{\n\t\t\t\tcollection, err := app.FindCollectionByNameOrId(\"bqnxb95f2cooowp\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tif !errors.Is(err, sql.ErrNoRows) {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfor _, record := range records {\n\t\t\t\t\t\t\tchanged := false\n\n\t\t\t\t\t\t\tnodeConfig := make(map[string]any)\n\t\t\t\t\t\t\tif err := record.UnmarshalJSONField(\"nodeConfig\", &nodeConfig); err == nil {\n\t\t\t\t\t\t\t\tif _, ok := nodeConfig[\"id\"]; ok {\n\t\t\t\t\t\t\t\t\tif _, ok := nodeConfig[\"type\"]; ok {\n\t\t\t\t\t\t\t\t\t\tif _, ok := nodeConfig[\"config\"]; ok {\n\t\t\t\t\t\t\t\t\t\t\trecord.Set(\"nodeConfig\", nodeConfig[\"config\"])\n\t\t\t\t\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\toutputs := make([]map[string]any, 0)\n\t\t\t\t\t\t\tif err := record.UnmarshalJSONField(\"outputs\", &outputs); err == nil {\n\t\t\t\t\t\t\t\tfor i, output := range outputs {\n\t\t\t\t\t\t\t\t\tif _, ok := output[\"label\"]; ok {\n\t\t\t\t\t\t\t\t\t\toutput[\"valueType\"] = \"string\"\n\t\t\t\t\t\t\t\t\t\tdelete(output, \"label\")\n\t\t\t\t\t\t\t\t\t\tdelete(output, \"required\")\n\t\t\t\t\t\t\t\t\t\tdelete(output, \"valueSelector\")\n\n\t\t\t\t\t\t\t\t\t\tif output[\"type\"] == \"certificate\" {\n\t\t\t\t\t\t\t\t\t\t\toutput[\"type\"] = \"ref\"\n\t\t\t\t\t\t\t\t\t\t\toutput[\"value\"] = fmt.Sprintf(\"certificate#%s\", output[\"value\"])\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\toutputs[i] = output\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\trecord.Set(\"outputs\", outputs)\n\t\t\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif changed {\n\t\t\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// normalize field `nodeId` in collection `workflow`, `workflow_run`, `workflow_output`, `workflow_logs`\n\t\t\tconst ATTEMPTS = 3\n\t\t\tfor i := 1; i <= ATTEMPTS; i++ {\n\t\t\t\tapp.DB().NewQuery(`UPDATE workflow SET graphDraft=REPLACE(graphDraft, '\"id\":\"-', '\"id\":\"')`).Execute()\n\t\t\t\tapp.DB().NewQuery(`UPDATE workflow SET graphDraft=REPLACE(graphDraft, '\"id\":\"_', '\"id\":\"')`).Execute()\n\t\t\t\tapp.DB().NewQuery(`UPDATE workflow SET graphContent=REPLACE(graphContent, '\"id\":\"-', '\"id\":\"')`).Execute()\n\t\t\t\tapp.DB().NewQuery(`UPDATE workflow SET graphContent=REPLACE(graphContent, '\"id\":\"_', '\"id\":\"')`).Execute()\n\n\t\t\t\tapp.DB().NewQuery(`UPDATE workflow_run SET graph=REPLACE(graph, '\"id\":\"-', '\"id\":\"')`).Execute()\n\t\t\t\tapp.DB().NewQuery(`UPDATE workflow_run SET graph=REPLACE(graph, '\"id\":\"_', '\"id\":\"')`).Execute()\n\n\t\t\t\tapp.DB().NewQuery(`UPDATE workflow_output SET nodeId=SUBSTR(nodeId, 2) WHERE nodeId LIKE '-%'`).Execute()\n\t\t\t\tapp.DB().NewQuery(`UPDATE workflow_output SET nodeId=SUBSTR(nodeId, 2) WHERE nodeId LIKE '\\_%' ESCAPE '\\'`).Execute()\n\n\t\t\t\tapp.DB().NewQuery(`UPDATE workflow_logs SET nodeId=SUBSTR(nodeId, 2) WHERE nodeId LIKE '-%'`).Execute()\n\t\t\t\tapp.DB().NewQuery(`UPDATE workflow_logs SET nodeId=SUBSTR(nodeId, 2) WHERE nodeId LIKE '\\_%' ESCAPE '\\'`).Execute()\n\t\t\t}\n\t\t}\n\n\t\ttracer.Printf(\"done\")\n\t\treturn nil\n\t}, func(app core.App) error {\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "migrations/1757476801_initialize_v0.4.0.go",
    "content": "package migrations\n\nimport (\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\tm \"github.com/pocketbase/pocketbase/migrations\"\n)\n\nfunc init() {\n\tm.Register(func(app core.App) error {\n\t\tif err := app.DB().\n\t\t\tNewQuery(\"SELECT (1) FROM _migrations WHERE file={:file} LIMIT 1\").\n\t\t\tBind(dbx.Params{\"file\": \"1757476801_m0.4.0_initialize.go\"}).\n\t\t\tOne(&struct{}{}); err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\t// snapshot\n\t\t{\n\t\t\tjsonData := `[\n\t\t\t\t{\n\t\t\t\t\t\"fields\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{15}\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\t\t\t\"max\": 15,\n\t\t\t\t\t\t\t\"min\": 15,\n\t\t\t\t\t\t\t\"name\": \"id\",\n\t\t\t\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\t\t\t\"required\": true,\n\t\t\t\t\t\t\t\"system\": true,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"geeur58v\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"name\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text2024822322\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"provider\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"iql7jpwx\",\n\t\t\t\t\t\t\t\"maxSize\": 2000000,\n\t\t\t\t\t\t\t\"name\": \"config\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"json\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text2859962647\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"reserve\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"lr33hiwg\",\n\t\t\t\t\t\t\t\"max\": \"\",\n\t\t\t\t\t\t\t\"min\": \"\",\n\t\t\t\t\t\t\t\"name\": \"deleted\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"date\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"autodate2990389176\",\n\t\t\t\t\t\t\t\"name\": \"created\",\n\t\t\t\t\t\t\t\"onCreate\": true,\n\t\t\t\t\t\t\t\"onUpdate\": false,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"autodate\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"autodate3332085495\",\n\t\t\t\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\t\t\t\"onCreate\": true,\n\t\t\t\t\t\t\t\"onUpdate\": true,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"autodate\"\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"id\": \"4yzbv8urny5ja1e\",\n\t\t\t\t\t\"indexes\": [\n\t\t\t\t\t\t\"CREATE INDEX ` + \"`\" + `idx_wkoST0j` + \"`\" + ` ON ` + \"`\" + `access` + \"`\" + ` (` + \"`\" + `name` + \"`\" + `)\",\n\t\t\t\t\t\t\"CREATE INDEX ` + \"`\" + `idx_frh0JT1Aqx` + \"`\" + ` ON ` + \"`\" + `access` + \"`\" + ` (` + \"`\" + `provider` + \"`\" + `)\"\n\t\t\t\t\t],\n\t\t\t\t\t\"name\": \"access\",\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"base\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"fields\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{15}\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\t\t\t\"max\": 15,\n\t\t\t\t\t\t\t\"min\": 15,\n\t\t\t\t\t\t\t\"name\": \"id\",\n\t\t\t\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\t\t\t\"required\": true,\n\t\t\t\t\t\t\t\"system\": true,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"1tcmdsdf\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"name\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"f9wyhypi\",\n\t\t\t\t\t\t\t\"maxSize\": 2000000,\n\t\t\t\t\t\t\t\"name\": \"content\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"json\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"autodate2990389176\",\n\t\t\t\t\t\t\t\"name\": \"created\",\n\t\t\t\t\t\t\t\"onCreate\": true,\n\t\t\t\t\t\t\t\"onUpdate\": false,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"autodate\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"autodate3332085495\",\n\t\t\t\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\t\t\t\"onCreate\": true,\n\t\t\t\t\t\t\t\"onUpdate\": true,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"autodate\"\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"id\": \"dy6ccjb60spfy6p\",\n\t\t\t\t\t\"indexes\": [\n\t\t\t\t\t\t\"CREATE UNIQUE INDEX ` + \"`\" + `idx_RO7X9Vw` + \"`\" + ` ON ` + \"`\" + `settings` + \"`\" + ` (` + \"`\" + `name` + \"`\" + `)\"\n\t\t\t\t\t],\n\t\t\t\t\t\"name\": \"settings\",\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"base\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"fields\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{15}\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\t\t\t\"max\": 15,\n\t\t\t\t\t\t\t\"min\": 15,\n\t\t\t\t\t\t\t\"name\": \"id\",\n\t\t\t\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\t\t\t\"required\": true,\n\t\t\t\t\t\t\t\"system\": true,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"fmjfn0yw\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"ca\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"exceptDomains\": null,\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"qqwijqzt\",\n\t\t\t\t\t\t\t\"name\": \"email\",\n\t\t\t\t\t\t\t\"onlyDomains\": null,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"email\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"genxqtii\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"privateKey\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"1aoia909\",\n\t\t\t\t\t\t\t\"maxSize\": 2000000,\n\t\t\t\t\t\t\t\"name\": \"acmeAccount\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"json\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"exceptDomains\": null,\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"url2424532088\",\n\t\t\t\t\t\t\t\"name\": \"acmeAcctUrl\",\n\t\t\t\t\t\t\t\"onlyDomains\": null,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"url\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"exceptDomains\": null,\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"url3632694140\",\n\t\t\t\t\t\t\t\"name\": \"acmeDirUrl\",\n\t\t\t\t\t\t\t\"onlyDomains\": null,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"url\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"autodate2990389176\",\n\t\t\t\t\t\t\t\"name\": \"created\",\n\t\t\t\t\t\t\t\"onCreate\": true,\n\t\t\t\t\t\t\t\"onUpdate\": false,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"autodate\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"autodate3332085495\",\n\t\t\t\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\t\t\t\"onCreate\": true,\n\t\t\t\t\t\t\t\"onUpdate\": true,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"autodate\"\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"id\": \"012d7abbod1hwvr\",\n\t\t\t\t\t\"indexes\": [\n\t\t\t\t\t\t\"CREATE INDEX ` + \"`\" + `idx_dQiYzimY7m` + \"`\" + ` ON ` + \"`\" + `acme_accounts` + \"`\" + ` (` + \"`\" + `ca` + \"`\" + `)\",\n\t\t\t\t\t\t\"CREATE INDEX ` + \"`\" + `idx_TjyqY6LAGa` + \"`\" + ` ON ` + \"`\" + `acme_accounts` + \"`\" + ` (\\n  ` + \"`\" + `ca` + \"`\" + `,\\n  ` + \"`\" + `acmeDirUrl` + \"`\" + `\\n)\",\n\t\t\t\t\t\t\"CREATE UNIQUE INDEX ` + \"`\" + `idx_G4brUDgxzc` + \"`\" + ` ON ` + \"`\" + `acme_accounts` + \"`\" + ` (\\n  ` + \"`\" + `ca` + \"`\" + `,\\n  ` + \"`\" + `acmeDirUrl` + \"`\" + `,\\n  ` + \"`\" + `acmeAcctUrl` + \"`\" + `\\n)\"\n\t\t\t\t\t],\n\t\t\t\t\t\"name\": \"acme_accounts\",\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"base\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"fields\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{15}\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\t\t\t\"max\": 15,\n\t\t\t\t\t\t\t\"min\": 15,\n\t\t\t\t\t\t\t\"name\": \"id\",\n\t\t\t\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\t\t\t\"required\": true,\n\t\t\t\t\t\t\t\"system\": true,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"8yydhv1h\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"name\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"1buzebwz\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"description\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"vqoajwjq\",\n\t\t\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\t\t\"name\": \"trigger\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"select\",\n\t\t\t\t\t\t\t\"values\": [\n\t\t\t\t\t\t\t\t\"manual\",\n\t\t\t\t\t\t\t\t\"scheduled\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"8ho247wh\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"triggerCron\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"nq7kfdzi\",\n\t\t\t\t\t\t\t\"name\": \"enabled\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"bool\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"g9ohkk5o\",\n\t\t\t\t\t\t\t\"maxSize\": 5000000,\n\t\t\t\t\t\t\t\"name\": \"graphDraft\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"json\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"awlphkfe\",\n\t\t\t\t\t\t\t\"maxSize\": 5000000,\n\t\t\t\t\t\t\t\"name\": \"graphContent\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"json\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"2rpfz9t3\",\n\t\t\t\t\t\t\t\"name\": \"hasDraft\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"bool\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"bool3832150317\",\n\t\t\t\t\t\t\t\"name\": \"hasContent\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"bool\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"cascadeDelete\": false,\n\t\t\t\t\t\t\t\"collectionId\": \"qjp8lygssgwyqyz\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"a23wkj9x\",\n\t\t\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\t\t\"name\": \"lastRunRef\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"zivdxh23\",\n\t\t\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\t\t\"name\": \"lastRunStatus\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"select\",\n\t\t\t\t\t\t\t\"values\": [\n\t\t\t\t\t\t\t\t\"pending\",\n\t\t\t\t\t\t\t\t\"processing\",\n\t\t\t\t\t\t\t\t\"succeeded\",\n\t\t\t\t\t\t\t\t\"failed\",\n\t\t\t\t\t\t\t\t\"canceled\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"u9bosu36\",\n\t\t\t\t\t\t\t\"max\": \"\",\n\t\t\t\t\t\t\t\"min\": \"\",\n\t\t\t\t\t\t\t\"name\": \"lastRunTime\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"date\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"autodate2990389176\",\n\t\t\t\t\t\t\t\"name\": \"created\",\n\t\t\t\t\t\t\t\"onCreate\": true,\n\t\t\t\t\t\t\t\"onUpdate\": false,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"autodate\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"autodate3332085495\",\n\t\t\t\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\t\t\t\"onCreate\": true,\n\t\t\t\t\t\t\t\"onUpdate\": true,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"autodate\"\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"id\": \"tovyif5ax6j62ur\",\n\t\t\t\t\t\"indexes\": [],\n\t\t\t\t\t\"name\": \"workflow\",\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"base\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"fields\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{15}\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\t\t\t\"max\": 15,\n\t\t\t\t\t\t\t\"min\": 15,\n\t\t\t\t\t\t\t\"name\": \"id\",\n\t\t\t\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\t\t\t\"required\": true,\n\t\t\t\t\t\t\t\"system\": true,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\t\t\t\"collectionId\": \"tovyif5ax6j62ur\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"jka88auc\",\n\t\t\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\t\t\"name\": \"workflowRef\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\t\t\t\"collectionId\": \"qjp8lygssgwyqyz\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"relation821863227\",\n\t\t\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\t\t\"name\": \"runRef\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"z9fgvqkz\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"nodeId\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"json2239752261\",\n\t\t\t\t\t\t\t\"maxSize\": 5000000,\n\t\t\t\t\t\t\t\"name\": \"nodeConfig\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"json\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"he4cceqb\",\n\t\t\t\t\t\t\t\"maxSize\": 5000000,\n\t\t\t\t\t\t\t\"name\": \"outputs\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"json\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"2yfxbxuf\",\n\t\t\t\t\t\t\t\"name\": \"succeeded\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"bool\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"autodate2990389176\",\n\t\t\t\t\t\t\t\"name\": \"created\",\n\t\t\t\t\t\t\t\"onCreate\": true,\n\t\t\t\t\t\t\t\"onUpdate\": false,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"autodate\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"autodate3332085495\",\n\t\t\t\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\t\t\t\"onCreate\": true,\n\t\t\t\t\t\t\t\"onUpdate\": true,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"autodate\"\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"id\": \"bqnxb95f2cooowp\",\n\t\t\t\t\t\"indexes\": [\n\t\t\t\t\t\t\"CREATE INDEX ` + \"`\" + `idx_BYoQPsz4my` + \"`\" + ` ON ` + \"`\" + `workflow_output` + \"`\" + ` (` + \"`\" + `workflowRef` + \"`\" + `)\",\n\t\t\t\t\t\t\"CREATE INDEX ` + \"`\" + `idx_O9zxLETuxJ` + \"`\" + ` ON ` + \"`\" + `workflow_output` + \"`\" + ` (` + \"`\" + `runRef` + \"`\" + `)\",\n\t\t\t\t\t\t\"CREATE INDEX ` + \"`\" + `idx_luac8Ul34G` + \"`\" + ` ON ` + \"`\" + `workflow_output` + \"`\" + ` (` + \"`\" + `nodeId` + \"`\" + `)\"\n\t\t\t\t\t],\n\t\t\t\t\t\"name\": \"workflow_output\",\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"base\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"fields\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{15}\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\t\t\t\"max\": 15,\n\t\t\t\t\t\t\t\"min\": 15,\n\t\t\t\t\t\t\t\"name\": \"id\",\n\t\t\t\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\t\t\t\"required\": true,\n\t\t\t\t\t\t\t\"system\": true,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"by9hetqi\",\n\t\t\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\t\t\"name\": \"source\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"select\",\n\t\t\t\t\t\t\t\"values\": [\n\t\t\t\t\t\t\t\t\"request\",\n\t\t\t\t\t\t\t\t\"upload\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"fugxf58p\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"subjectAltNames\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text2069360702\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"serialNumber\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"plmambpz\",\n\t\t\t\t\t\t\t\"max\": 100000,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"certificate\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"49qvwxcg\",\n\t\t\t\t\t\t\t\"max\": 100000,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"privateKey\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text2910474005\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"issuerOrg\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"agt7n5bb\",\n\t\t\t\t\t\t\t\"max\": 100000,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"issuerCertificate\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text4164403445\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"keyAlgorithm\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"v40aqzpd\",\n\t\t\t\t\t\t\t\"max\": \"\",\n\t\t\t\t\t\t\t\"min\": \"\",\n\t\t\t\t\t\t\t\"name\": \"validityNotBefore\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"date\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"zgpdby2k\",\n\t\t\t\t\t\t\t\"max\": \"\",\n\t\t\t\t\t\t\t\"min\": \"\",\n\t\t\t\t\t\t\t\"name\": \"validityNotAfter\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"date\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text2045248758\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"acmeAcctUrl\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"exceptDomains\": null,\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"ayyjy5ve\",\n\t\t\t\t\t\t\t\"name\": \"acmeCertUrl\",\n\t\t\t\t\t\t\t\"onlyDomains\": null,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"url\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"exceptDomains\": null,\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"3x5heo8e\",\n\t\t\t\t\t\t\t\"name\": \"acmeCertStableUrl\",\n\t\t\t\t\t\t\t\"onlyDomains\": null,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"url\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"bool810050391\",\n\t\t\t\t\t\t\t\"name\": \"acmeRenewed\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"bool\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"cascadeDelete\": false,\n\t\t\t\t\t\t\t\"collectionId\": \"tovyif5ax6j62ur\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"uvqfamb1\",\n\t\t\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\t\t\"name\": \"workflowRef\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"cascadeDelete\": false,\n\t\t\t\t\t\t\t\"collectionId\": \"qjp8lygssgwyqyz\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"relation3917999135\",\n\t\t\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\t\t\"name\": \"workflowRunRef\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"uqldzldw\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"workflowNodeId\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"klyf4nlq\",\n\t\t\t\t\t\t\t\"max\": \"\",\n\t\t\t\t\t\t\t\"min\": \"\",\n\t\t\t\t\t\t\t\"name\": \"deleted\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"date\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"autodate2990389176\",\n\t\t\t\t\t\t\t\"name\": \"created\",\n\t\t\t\t\t\t\t\"onCreate\": true,\n\t\t\t\t\t\t\t\"onUpdate\": false,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"autodate\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"autodate3332085495\",\n\t\t\t\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\t\t\t\"onCreate\": true,\n\t\t\t\t\t\t\t\"onUpdate\": true,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"autodate\"\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"id\": \"4szxr9x43tpj6np\",\n\t\t\t\t\t\"indexes\": [\n\t\t\t\t\t\t\"CREATE INDEX ` + \"`\" + `idx_Jx8TXzDCmw` + \"`\" + ` ON ` + \"`\" + `certificate` + \"`\" + ` (` + \"`\" + `workflowRef` + \"`\" + `)\",\n\t\t\t\t\t\t\"CREATE INDEX ` + \"`\" + `idx_2cRXqNDyyp` + \"`\" + ` ON ` + \"`\" + `certificate` + \"`\" + ` (` + \"`\" + `workflowRunRef` + \"`\" + `)\",\n\t\t\t\t\t\t\"CREATE INDEX ` + \"`\" + `idx_kcKpgAZapk` + \"`\" + ` ON ` + \"`\" + `certificate` + \"`\" + ` (` + \"`\" + `workflowNodeId` + \"`\" + `)\"\n\t\t\t\t\t],\n\t\t\t\t\t\"name\": \"certificate\",\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"base\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"fields\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{15}\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\t\t\t\"max\": 15,\n\t\t\t\t\t\t\t\"min\": 15,\n\t\t\t\t\t\t\t\"name\": \"id\",\n\t\t\t\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\t\t\t\"required\": true,\n\t\t\t\t\t\t\t\"system\": true,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\t\t\t\"collectionId\": \"tovyif5ax6j62ur\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"m8xfsyyy\",\n\t\t\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\t\t\"name\": \"workflowRef\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"qldmh0tw\",\n\t\t\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\t\t\"name\": \"status\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"select\",\n\t\t\t\t\t\t\t\"values\": [\n\t\t\t\t\t\t\t\t\"pending\",\n\t\t\t\t\t\t\t\t\"processing\",\n\t\t\t\t\t\t\t\t\"succeeded\",\n\t\t\t\t\t\t\t\t\"failed\",\n\t\t\t\t\t\t\t\t\"canceled\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"jlroa3fk\",\n\t\t\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\t\t\"name\": \"trigger\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"select\",\n\t\t\t\t\t\t\t\"values\": [\n\t\t\t\t\t\t\t\t\"manual\",\n\t\t\t\t\t\t\t\t\"scheduled\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"k9xvtf89\",\n\t\t\t\t\t\t\t\"max\": \"\",\n\t\t\t\t\t\t\t\"min\": \"\",\n\t\t\t\t\t\t\t\"name\": \"startedAt\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"date\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"3ikum7mk\",\n\t\t\t\t\t\t\t\"max\": \"\",\n\t\t\t\t\t\t\t\"min\": \"\",\n\t\t\t\t\t\t\t\"name\": \"endedAt\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"date\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"json772177811\",\n\t\t\t\t\t\t\t\"maxSize\": 5000000,\n\t\t\t\t\t\t\t\"name\": \"graph\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"json\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"hvebkuxw\",\n\t\t\t\t\t\t\t\"max\": 20000,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"error\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"autodate2990389176\",\n\t\t\t\t\t\t\t\"name\": \"created\",\n\t\t\t\t\t\t\t\"onCreate\": true,\n\t\t\t\t\t\t\t\"onUpdate\": false,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"autodate\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"autodate3332085495\",\n\t\t\t\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\t\t\t\"onCreate\": true,\n\t\t\t\t\t\t\t\"onUpdate\": true,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"autodate\"\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"id\": \"qjp8lygssgwyqyz\",\n\t\t\t\t\t\"indexes\": [\n\t\t\t\t\t\t\"CREATE INDEX ` + \"`\" + `idx_7ZpfjTFsD2` + \"`\" + ` ON ` + \"`\" + `workflow_run` + \"`\" + ` (` + \"`\" + `workflowRef` + \"`\" + `)\"\n\t\t\t\t\t],\n\t\t\t\t\t\"name\": \"workflow_run\",\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"base\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"fields\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{15}\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\t\t\t\"max\": 15,\n\t\t\t\t\t\t\t\"min\": 15,\n\t\t\t\t\t\t\t\"name\": \"id\",\n\t\t\t\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\t\t\t\"required\": true,\n\t\t\t\t\t\t\t\"system\": true,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\t\t\t\"collectionId\": \"tovyif5ax6j62ur\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"relation3371272342\",\n\t\t\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\t\t\"name\": \"workflowRef\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\t\t\t\"collectionId\": \"qjp8lygssgwyqyz\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"relation821863227\",\n\t\t\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\t\t\"name\": \"runRef\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text157423495\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"nodeId\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text3227511481\",\n\t\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"nodeName\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"number2782324286\",\n\t\t\t\t\t\t\t\"max\": null,\n\t\t\t\t\t\t\t\"min\": null,\n\t\t\t\t\t\t\t\"name\": \"timestamp\",\n\t\t\t\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"number\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"number760395071\",\n\t\t\t\t\t\t\t\"max\": null,\n\t\t\t\t\t\t\t\"min\": null,\n\t\t\t\t\t\t\t\"name\": \"level\",\n\t\t\t\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"number\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"text3065852031\",\n\t\t\t\t\t\t\t\"max\": 20000,\n\t\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\t\"name\": \"message\",\n\t\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"json2918445923\",\n\t\t\t\t\t\t\t\"maxSize\": 5000000,\n\t\t\t\t\t\t\t\"name\": \"data\",\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"json\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\t\"id\": \"autodate2990389176\",\n\t\t\t\t\t\t\t\"name\": \"created\",\n\t\t\t\t\t\t\t\"onCreate\": true,\n\t\t\t\t\t\t\t\"onUpdate\": false,\n\t\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\t\"type\": \"autodate\"\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"id\": \"pbc_1682296116\",\n\t\t\t\t\t\"indexes\": [\n\t\t\t\t\t\t\"CREATE INDEX ` + \"`\" + `idx_IOlpy6XuJ2` + \"`\" + ` ON ` + \"`\" + `workflow_logs` + \"`\" + ` (` + \"`\" + `workflowRef` + \"`\" + `)\",\n\t\t\t\t\t\t\"CREATE INDEX ` + \"`\" + `idx_qVlTb2yl7v` + \"`\" + ` ON ` + \"`\" + `workflow_logs` + \"`\" + ` (` + \"`\" + `runRef` + \"`\" + `)\",\n\t\t\t\t\t\t\"CREATE INDEX ` + \"`\" + `idx_UL4tdCXNlA` + \"`\" + ` ON ` + \"`\" + `workflow_logs` + \"`\" + ` (` + \"`\" + `nodeId` + \"`\" + `)\"\n\t\t\t\t\t],\n\t\t\t\t\t\"name\": \"workflow_logs\",\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"base\"\n\t\t\t\t}\n\t\t\t]`\n\n\t\t\tif err := app.ImportCollectionsByMarshaledJSON([]byte(jsonData), false); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// initialize superuser\n\t\t{\n\t\t\tcollection, err := app.FindCollectionByNameOrId(core.CollectionNameSuperusers)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif len(records) == 0 {\n\t\t\t\tenvUsername := strings.TrimSpace(os.Getenv(\"CERTIMATE_ADMIN_USERNAME\"))\n\t\t\t\tif envUsername == \"\" {\n\t\t\t\t\tenvUsername = \"admin@certimate.fun\"\n\t\t\t\t}\n\n\t\t\t\tenvPassword := strings.TrimSpace(os.Getenv(\"CERTIMATE_ADMIN_PASSWORD\"))\n\t\t\t\tif envPassword == \"\" {\n\t\t\t\t\tenvPassword = \"1234567890\"\n\t\t\t\t}\n\n\t\t\t\trecord := core.NewRecord(collection)\n\t\t\t\trecord.Set(\"email\", envUsername)\n\t\t\t\trecord.Set(\"password\", envPassword)\n\t\t\t\treturn app.Save(record)\n\t\t\t}\n\t\t}\n\n\t\t// clean old migrations\n\t\t{\n\t\t\tmigrations := []string{\n\t\t\t\t\"1739462400_collections_snapshot.go\",\n\t\t\t\t\"1739462401_superusers_initial.go\",\n\t\t\t\t\"1740050400_upgrade.go\",\n\t\t\t\t\"1742209200_upgrade.go\",\n\t\t\t\t\"1742392800_upgrade.go\",\n\t\t\t\t\"1742644800_upgrade.go\",\n\t\t\t\t\"1743264000_upgrade.go\",\n\t\t\t\t\"1744192800_upgrade.go\",\n\t\t\t\t\"1744459000_upgrade.go\",\n\t\t\t\t\"1745308800_upgrade.go\",\n\t\t\t\t\"1745726400_upgrade.go\",\n\t\t\t\t\"1747314000_upgrade.go\",\n\t\t\t\t\"1747389600_upgrade.go\",\n\t\t\t\t\"1748178000_upgrade.go\",\n\t\t\t\t\"1748228400_upgrade.go\",\n\t\t\t\t\"1748959200_upgrade.go\",\n\t\t\t\t\"1750687200_upgrade.go\",\n\t\t\t\t\"1751961600_upgrade.go\",\n\t\t\t\t\"1753272000_v0.4.0_migrate.go\",\n\t\t\t\t\"1755187200_cm0.4.0_migrate.go\",\n\t\t\t\t\"1756296000_cm0.4.0_migrate.go\",\n\t\t\t\t\"1757476800_cm0.4.0_initialize.go\",\n\t\t\t\t\"1757476800_m0.4.0_migrate.go\",\n\t\t\t\t\"1757476801_m0.4.0_initialize.go\",\n\t\t\t\t\"1760486400_m0.4.1.go\",\n\t\t\t\t\"1762142400_m0.4.3.go\",\n\t\t\t\t\"1762516800_m0.4.4.go\",\n\t\t\t\t\"1763373600_m0.4.5.go\",\n\t\t\t\t\"1763640000_m0.4.6.go\",\n\t\t\t}\n\t\t\tfor _, name := range migrations {\n\t\t\t\tapp.DB().NewQuery(\"DELETE FROM _migrations WHERE file='\" + name + \"'\").Execute()\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}, func(app core.App) error {\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "migrations/1760486400_upgrade_v0.4.1.go",
    "content": "package migrations\n\nimport (\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\tm \"github.com/pocketbase/pocketbase/migrations\"\n\n\tsnaps \"github.com/certimate-go/certimate/migrations/snaps/v0.4\"\n)\n\nfunc init() {\n\tm.Register(func(app core.App) error {\n\t\tif err := app.DB().\n\t\t\tNewQuery(\"SELECT (1) FROM _migrations WHERE file={:file} LIMIT 1\").\n\t\t\tBind(dbx.Params{\"file\": \"1760486400_m0.4.1.go\"}).\n\t\t\tOne(&struct{}{}); err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\ttracer := NewTracer(\"v0.4.1\")\n\t\ttracer.Printf(\"go ...\")\n\n\t\t// adapt to new workflow data structure\n\t\t{\n\t\t\twalker := &snaps.WorkflowGraphWalker{}\n\t\t\twalker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) {\n\t\t\t\t_changed = false\n\t\t\t\t_err = nil\n\n\t\t\t\tif node.Type != \"bizDeploy\" {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tnodeCfg := node.Data.Config\n\n\t\t\t\tswitch nodeCfg[\"provider\"] {\n\t\t\t\tcase \"local\":\n\t\t\t\t\t{\n\t\t\t\t\t\tif nodeCfg[\"providerAccessId\"] != nil {\n\t\t\t\t\t\t\tdelete(nodeCfg, \"providerAccessId\")\n\n\t\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t})\n\n\t\t\t// update collection `workflow`\n\t\t\t//   - fix #982\n\t\t\t{\n\t\t\t\tcollection, err := app.FindCollectionByNameOrId(\"tovyif5ax6j62ur\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfor _, record := range records {\n\t\t\t\t\tchanged := false\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graphDraft\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graphContent\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif changed {\n\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttracer.Printf(\"done\")\n\t\treturn nil\n\t}, func(app core.App) error {\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "migrations/1762142400_upgrade_v0.4.3.go",
    "content": "package migrations\n\nimport (\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\tm \"github.com/pocketbase/pocketbase/migrations\"\n\n\tsnaps \"github.com/certimate-go/certimate/migrations/snaps/v0.4\"\n)\n\nfunc init() {\n\tm.Register(func(app core.App) error {\n\t\tif err := app.DB().\n\t\t\tNewQuery(\"SELECT (1) FROM _migrations WHERE file={:file} LIMIT 1\").\n\t\t\tBind(dbx.Params{\"file\": \"1762142400_m0.4.3.go\"}).\n\t\t\tOne(&struct{}{}); err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\ttracer := NewTracer(\"v0.4.3\")\n\t\ttracer.Printf(\"go ...\")\n\n\t\t// update collection `certificate`\n\t\t//   - rename field `acmeRenewed` to `isRenewed`\n\t\t//   - add field `isRevoked`\n\t\t//   - add field `validityInterval`\n\t\t{\n\t\t\tcollection, err := app.FindCollectionByNameOrId(\"4szxr9x43tpj6np\")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := collection.Fields.AddMarshaledJSONAt(11, []byte(`{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"number2453290051\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"validityInterval\",\n\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t}`)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := collection.Fields.AddMarshaledJSONAt(14, []byte(`{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"bool810050391\",\n\t\t\t\t\"name\": \"isRenewed\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"bool\"\n\t\t\t}`)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := collection.Fields.AddMarshaledJSONAt(15, []byte(`{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"bool3680845581\",\n\t\t\t\t\"name\": \"isRevoked\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"bool\"\n\t\t\t}`)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := app.Save(collection); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif _, err := app.DB().NewQuery(\"UPDATE certificate SET validityInterval = (STRFTIME('%s', validityNotAfter) - STRFTIME('%s', validityNotBefore))\").Execute(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\ttracer.Printf(\"collection '%s' updated\", collection.Name)\n\t\t}\n\n\t\t// adapt to new workflow data structure\n\t\t{\n\t\t\twalker := &snaps.WorkflowGraphWalker{}\n\t\t\twalker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) {\n\t\t\t\t_changed = false\n\t\t\t\t_err = nil\n\n\t\t\t\tif node.Type != \"bizApply\" {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tnodeCfg := node.Data.Config\n\n\t\t\t\tif nodeCfg[\"keySource\"] == nil || nodeCfg[\"keySource\"] == \"\" {\n\t\t\t\t\tnodeCfg[\"keySource\"] = \"auto\"\n\n\t\t\t\t\t_changed = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t})\n\t\t\twalker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) {\n\t\t\t\t_changed = false\n\t\t\t\t_err = nil\n\n\t\t\t\tif node.Type != \"bizUpload\" {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tnodeCfg := node.Data.Config\n\n\t\t\t\tif nodeCfg[\"source\"] == nil || nodeCfg[\"source\"] == \"\" {\n\t\t\t\t\tnodeCfg[\"source\"] = \"form\"\n\n\t\t\t\t\t_changed = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t})\n\n\t\t\t// update collection `workflow`\n\t\t\t//   - migrate field `graphDraft` / `graphContent`\n\t\t\t{\n\t\t\t\tcollection, err := app.FindCollectionByNameOrId(\"tovyif5ax6j62ur\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfor _, record := range records {\n\t\t\t\t\tchanged := false\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graphDraft\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graphContent\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif changed {\n\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// update collection `workflow_run`\n\t\t\t//   - migrate field `graph`\n\t\t\t{\n\t\t\t\tcollection, err := app.FindCollectionByNameOrId(\"qjp8lygssgwyqyz\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfor _, record := range records {\n\t\t\t\t\tchanged := false\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graph\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif changed {\n\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttracer.Printf(\"done\")\n\t\treturn nil\n\t}, func(app core.App) error {\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "migrations/1762516800_upgrade_v0.4.4.go",
    "content": "package migrations\n\nimport (\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\tm \"github.com/pocketbase/pocketbase/migrations\"\n)\n\nfunc init() {\n\tm.Register(func(app core.App) error {\n\t\tif err := app.DB().\n\t\t\tNewQuery(\"SELECT (1) FROM _migrations WHERE file={:file} LIMIT 1\").\n\t\t\tBind(dbx.Params{\"file\": \"1762516800_m0.4.4.go\"}).\n\t\t\tOne(&struct{}{}); err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\ttracer := NewTracer(\"v0.4.4\")\n\t\ttracer.Printf(\"go ...\")\n\n\t\t// update collection `access`\n\t\t//   - fix #1027\n\t\t{\n\t\t\tif _, err := app.DB().NewQuery(\"UPDATE access SET provider = 'hostingde' WHERE provider = 'hostingDE'\").Execute(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// update collection `workflow`\n\t\t//   - fix #1027\n\t\t{\n\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow SET graphDraft = REPLACE(graphDraft, '\\\"hostingDE\\\"', '\\\"hostingde\\\"')\").Execute(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow SET graphContent = REPLACE(graphContent, '\\\"hostingDE\\\"', '\\\"hostingde\\\"')\").Execute(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// update collection `settings`\n\t\t//   - modify field `content` schema of `persistence`\n\t\t{\n\t\t\tcollection, err := app.FindCollectionByNameOrId(\"dy6ccjb60spfy6p\")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\trecords, err := app.FindRecordsByFilter(collection, \"name=\\\"persistence\\\"\", \"\", 1, 0)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t} else if len(records) != 0 {\n\t\t\t\trecord := records[0]\n\t\t\t\tchanged := false\n\n\t\t\t\tcontent := make(map[string]any)\n\t\t\t\tif err := record.UnmarshalJSONField(\"content\", &content); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t} else {\n\t\t\t\t\tif _, ok := content[\"expiredCertificatesMaxDaysRetention\"]; ok {\n\t\t\t\t\t\tcontent[\"certificatesRetentionMaxDays\"] = content[\"expiredCertificatesMaxDaysRetention\"]\n\t\t\t\t\t\tdelete(content, \"expiredCertificatesMaxDaysRetention\")\n\n\t\t\t\t\t\trecord.Set(\"content\", content)\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\n\t\t\t\t\tif _, ok := content[\"workflowRunsMaxDaysRetention\"]; ok {\n\t\t\t\t\t\tcontent[\"workflowRunsRetentionMaxDays\"] = content[\"workflowRunsMaxDaysRetention\"]\n\t\t\t\t\t\tdelete(content, \"workflowRunsMaxDaysRetention\")\n\n\t\t\t\t\t\trecord.Set(\"content\", content)\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif changed {\n\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttracer.Printf(\"done\")\n\t\treturn nil\n\t}, func(app core.App) error {\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "migrations/1763373600_upgrade_v0.4.5.go",
    "content": "package migrations\n\nimport (\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\tm \"github.com/pocketbase/pocketbase/migrations\"\n\n\tsnaps \"github.com/certimate-go/certimate/migrations/snaps/v0.4\"\n)\n\nfunc init() {\n\tm.Register(func(app core.App) error {\n\t\tif err := app.DB().\n\t\t\tNewQuery(\"SELECT (1) FROM _migrations WHERE file={:file} LIMIT 1\").\n\t\t\tBind(dbx.Params{\"file\": \"1763373600_m0.4.5.go\"}).\n\t\t\tOne(&struct{}{}); err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\ttracer := NewTracer(\"v0.4.5\")\n\t\ttracer.Printf(\"go ...\")\n\n\t\t// adapt to new workflow data structure\n\t\t{\n\t\t\twalker := &snaps.WorkflowGraphWalker{}\n\t\t\twalker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) {\n\t\t\t\t_changed = false\n\t\t\t\t_err = nil\n\n\t\t\t\tif node.Type != \"bizDeploy\" {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tnodeCfg := node.Data.Config\n\n\t\t\t\tswitch nodeCfg[\"provider\"] {\n\t\t\t\tcase \"aliyun-waf\":\n\t\t\t\t\t{\n\t\t\t\t\t\tif providerCfg, ok := nodeCfg[\"providerConfig\"].(map[string]any); ok {\n\t\t\t\t\t\t\tproviderCfg[\"serviceType\"] = \"cname\"\n\t\t\t\t\t\t\tnodeCfg[\"providerConfig\"] = providerCfg\n\n\t\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\tcase \"baishan-cdn\":\n\t\t\t\tcase \"ksyun-cdn\":\n\t\t\t\tcase \"rainyun-rcdn\":\n\t\t\t\t\t{\n\t\t\t\t\t\tif providerCfg, ok := nodeCfg[\"providerConfig\"].(map[string]any); ok {\n\t\t\t\t\t\t\tif providerCfg[\"certificateId\"] != nil && providerCfg[\"certificateId\"].(string) != \"\" {\n\t\t\t\t\t\t\t\tproviderCfg[\"resourceType\"] = \"certificate\"\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tproviderCfg[\"resourceType\"] = \"domain\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tnodeCfg[\"providerConfig\"] = providerCfg\n\n\t\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\tcase \"tencentcloud-ssldeploy\":\n\t\t\t\t\t{\n\t\t\t\t\t\tif providerCfg, ok := nodeCfg[\"providerConfig\"].(map[string]any); ok {\n\t\t\t\t\t\t\tproviderCfg[\"resourceProduct\"] = providerCfg[\"resourceType\"]\n\t\t\t\t\t\t\tdelete(providerCfg, \"resourceType\")\n\t\t\t\t\t\t\tnodeCfg[\"providerConfig\"] = providerCfg\n\n\t\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\tcase \"tencentcloud-sslupdate\":\n\t\t\t\t\t{\n\t\t\t\t\t\tif providerCfg, ok := nodeCfg[\"providerConfig\"].(map[string]any); ok {\n\t\t\t\t\t\t\tproviderCfg[\"resourceProducts\"] = providerCfg[\"resourceTypes\"]\n\t\t\t\t\t\t\tdelete(providerCfg, \"resourceTypes\")\n\t\t\t\t\t\t\tnodeCfg[\"providerConfig\"] = providerCfg\n\n\t\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t})\n\n\t\t\t// update collection `workflow`\n\t\t\t//   - migrate field `graphDraft` / `graphContent`\n\t\t\t{\n\t\t\t\tcollection, err := app.FindCollectionByNameOrId(\"tovyif5ax6j62ur\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfor _, record := range records {\n\t\t\t\t\tchanged := false\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graphDraft\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graphContent\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif changed {\n\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow SET graphDraft = REPLACE(graphDraft, '\\\"matchPattern\\\"', '\\\"domainMatchPattern\\\"')\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow SET graphContent = REPLACE(graphContent, '\\\"matchPattern\\\"', '\\\"domainMatchPattern\\\"')\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// update collection `workflow_run`\n\t\t\t//   - migrate field `graph`\n\t\t\t{\n\t\t\t\tcollection, err := app.FindCollectionByNameOrId(\"qjp8lygssgwyqyz\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfor _, record := range records {\n\t\t\t\t\tchanged := false\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graph\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif changed {\n\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow_run SET graph = REPLACE(graph, '\\\"matchPattern\\\"', '\\\"domainMatchPattern\\\"')\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// update collection `workflow_output`\n\t\t\t//   - migrate field `nodeConfig`\n\t\t\t{\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow_output SET nodeConfig = REPLACE(nodeConfig, '\\\"matchPattern\\\"', '\\\"domainMatchPattern\\\"')\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow_output SET nodeConfig = REPLACE(nodeConfig, '\\\"resourceType\\\"', '\\\"resourceProduct\\\"') WHERE nodeConfig LIKE '%\\\"provider\\\":\\\"tencentcloud-ssldeploy\\\"%' OR nodeConfig LIKE '%\\\"provider\\\":\\\"tencentcloud-sslupdate\\\"%'\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttracer.Printf(\"done\")\n\t\treturn nil\n\t}, func(app core.App) error {\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "migrations/1763640000_upgrade_v0.4.6.go",
    "content": "package migrations\n\nimport (\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\tm \"github.com/pocketbase/pocketbase/migrations\"\n\n\tsnaps \"github.com/certimate-go/certimate/migrations/snaps/v0.4\"\n)\n\nfunc init() {\n\tm.Register(func(app core.App) error {\n\t\tif err := app.DB().\n\t\t\tNewQuery(\"SELECT (1) FROM _migrations WHERE file={:file} LIMIT 1\").\n\t\t\tBind(dbx.Params{\"file\": \"1763640000_m0.4.6.go\"}).\n\t\t\tOne(&struct{}{}); err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\ttracer := NewTracer(\"v0.4.6\")\n\t\ttracer.Printf(\"go ...\")\n\n\t\t// adapt to new workflow data structure\n\t\t{\n\t\t\twalker := &snaps.WorkflowGraphWalker{}\n\t\t\twalker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) {\n\t\t\t\t_changed = false\n\t\t\t\t_err = nil\n\n\t\t\t\tif node.Type != \"bizDeploy\" {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tnodeCfg := node.Data.Config\n\n\t\t\t\tswitch nodeCfg[\"provider\"] {\n\t\t\t\tcase \"1panel-site\":\n\t\t\t\t\t{\n\t\t\t\t\t\tif providerCfg, ok := nodeCfg[\"providerConfig\"].(map[string]any); ok {\n\t\t\t\t\t\t\tif providerCfg[\"websiteId\"] != nil && providerCfg[\"websiteId\"].(string) != \"\" {\n\t\t\t\t\t\t\t\tproviderCfg[\"websiteMatchPattern\"] = \"specified\"\n\t\t\t\t\t\t\t\tnodeCfg[\"providerConfig\"] = providerCfg\n\n\t\t\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\tcase \"baotapanel-site\":\n\t\t\t\t\t{\n\t\t\t\t\t\tif providerCfg, ok := nodeCfg[\"providerConfig\"].(map[string]any); ok {\n\t\t\t\t\t\t\tif providerCfg[\"siteType\"] == nil || providerCfg[\"siteType\"].(string) == \"other\" {\n\t\t\t\t\t\t\t\tproviderCfg[\"siteType\"] = \"any\"\n\t\t\t\t\t\t\t\tnodeCfg[\"providerConfig\"] = providerCfg\n\n\t\t\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif providerCfg[\"siteNames\"] == nil || providerCfg[\"siteNames\"].(string) == \"\" {\n\t\t\t\t\t\t\t\tproviderCfg[\"siteNames\"] = providerCfg[\"siteName\"]\n\t\t\t\t\t\t\t\tdelete(providerCfg, \"siteName\")\n\t\t\t\t\t\t\t\tnodeCfg[\"providerConfig\"] = providerCfg\n\n\t\t\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif _changed {\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\tcase \"baotapanelgo-site\":\n\t\t\t\t\t{\n\t\t\t\t\t\tif providerCfg, ok := nodeCfg[\"providerConfig\"].(map[string]any); ok {\n\t\t\t\t\t\t\tif providerCfg[\"siteNames\"] == nil || providerCfg[\"siteNames\"].(string) == \"\" {\n\t\t\t\t\t\t\t\tproviderCfg[\"siteType\"] = \"php\"\n\t\t\t\t\t\t\t\tproviderCfg[\"siteNames\"] = providerCfg[\"siteName\"]\n\t\t\t\t\t\t\t\tdelete(providerCfg, \"siteName\")\n\t\t\t\t\t\t\t\tnodeCfg[\"providerConfig\"] = providerCfg\n\n\t\t\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\tcase \"baotawaf-site\":\n\t\t\t\t\t{\n\t\t\t\t\t\tif providerCfg, ok := nodeCfg[\"providerConfig\"].(map[string]any); ok {\n\t\t\t\t\t\t\tif providerCfg[\"siteNames\"] == nil || providerCfg[\"siteNames\"].(string) == \"\" {\n\t\t\t\t\t\t\t\tproviderCfg[\"siteNames\"] = providerCfg[\"siteName\"]\n\t\t\t\t\t\t\t\tdelete(providerCfg, \"siteName\")\n\t\t\t\t\t\t\t\tnodeCfg[\"providerConfig\"] = providerCfg\n\n\t\t\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\tcase \"ratpanel-site\":\n\t\t\t\t\t{\n\t\t\t\t\t\tif providerCfg, ok := nodeCfg[\"providerConfig\"].(map[string]any); ok {\n\t\t\t\t\t\t\tif providerCfg[\"siteNames\"] == nil || providerCfg[\"siteNames\"].(string) == \"\" {\n\t\t\t\t\t\t\t\tproviderCfg[\"siteNames\"] = providerCfg[\"siteName\"]\n\t\t\t\t\t\t\t\tdelete(providerCfg, \"siteName\")\n\t\t\t\t\t\t\t\tnodeCfg[\"providerConfig\"] = providerCfg\n\n\t\t\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\tcase \"safeline\":\n\t\t\t\t\t{\n\t\t\t\t\t\tnodeCfg[\"provider\"] = \"safeline-site\"\n\n\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t})\n\n\t\t\t// update collection `workflow`\n\t\t\t//   - migrate field `graphDraft` / `graphContent`\n\t\t\t{\n\t\t\t\tcollection, err := app.FindCollectionByNameOrId(\"tovyif5ax6j62ur\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfor _, record := range records {\n\t\t\t\t\tchanged := false\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graphDraft\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graphContent\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif changed {\n\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// update collection `workflow_run`\n\t\t\t//   - migrate field `graph`\n\t\t\t{\n\t\t\t\tcollection, err := app.FindCollectionByNameOrId(\"qjp8lygssgwyqyz\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfor _, record := range records {\n\t\t\t\t\tchanged := false\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graph\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif changed {\n\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// update collection `workflow_output`\n\t\t\t//   - migrate field `nodeConfig`\n\t\t\t{\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow_output SET nodeConfig = REPLACE(nodeConfig, '\\\"provider\\\":\\\"safeline\\\"', '\\\"provider\\\":\\\"safeline-site\\\"') WHERE nodeConfig LIKE '%\\\"provider\\\":\\\"safeline\\\"%'\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif _, err := app.DB().NewQuery(\"UPDATE workflow_output SET nodeConfig = REPLACE(nodeConfig, '\\\"siteName\\\":', '\\\"siteNames\\\":')\").Execute(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttracer.Printf(\"done\")\n\t\treturn nil\n\t}, func(app core.App) error {\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "migrations/1766592000_upgrade_v0.4.11.go",
    "content": "package migrations\n\nimport (\n\t\"net\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\tm \"github.com/pocketbase/pocketbase/migrations\"\n\n\tsnaps \"github.com/certimate-go/certimate/migrations/snaps/v0.4\"\n)\n\nfunc init() {\n\tm.Register(func(app core.App) error {\n\t\ttracer := NewTracer(\"v0.4.11\")\n\t\ttracer.Printf(\"go ...\")\n\n\t\t// adapt to new workflow data structure\n\t\t{\n\t\t\twalker := &snaps.WorkflowGraphWalker{}\n\t\t\twalker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) {\n\t\t\t\t_changed = false\n\t\t\t\t_err = nil\n\n\t\t\t\tif node.Type != \"bizApply\" {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tnodeCfg := node.Data.Config\n\n\t\t\t\tif nodeCfg[\"identifier\"] == nil || nodeCfg[\"identifier\"] == \"\" {\n\t\t\t\t\tif nodeCfg[\"domains\"] != nil && nodeCfg[\"domains\"].(string) != \"\" {\n\t\t\t\t\t\tif ip := net.ParseIP(nodeCfg[\"domains\"].(string)); ip != nil {\n\t\t\t\t\t\t\tnodeCfg[\"identifier\"] = \"ip\"\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tnodeCfg[\"identifier\"] = \"domain\"\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t})\n\t\t\twalker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) {\n\t\t\t\t_changed = false\n\t\t\t\t_err = nil\n\n\t\t\t\tif node.Type != \"bizUpload\" {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tnodeCfg := node.Data.Config\n\n\t\t\t\tif nodeCfg[\"domains\"] != nil {\n\t\t\t\t\tdelete(nodeCfg, \"domains\")\n\n\t\t\t\t\t_changed = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t})\n\n\t\t\t// update collection `workflow`\n\t\t\t//   - migrate field `graphDraft` / `graphContent`\n\t\t\t{\n\t\t\t\tcollection, err := app.FindCollectionByNameOrId(\"tovyif5ax6j62ur\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfor _, record := range records {\n\t\t\t\t\tchanged := false\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graphDraft\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graphContent\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif changed {\n\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// update collection `workflow_run`\n\t\t\t//   - migrate field `graph`\n\t\t\t{\n\t\t\t\tcollection, err := app.FindCollectionByNameOrId(\"qjp8lygssgwyqyz\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfor _, record := range records {\n\t\t\t\t\tchanged := false\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graph\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif changed {\n\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// clean old migrations\n\t\t{\n\t\t\tmigrations := []string{\n\t\t\t\t\"1757476800_m0.4.0_migrate.go\",\n\t\t\t\t\"1757476801_m0.4.0_initialize.go\",\n\t\t\t\t\"1760486400_m0.4.1.go\",\n\t\t\t\t\"1762142400_m0.4.3.go\",\n\t\t\t\t\"1762516800_m0.4.4.go\",\n\t\t\t\t\"1763373600_m0.4.5.go\",\n\t\t\t\t\"1763553600_m0.4.6.go\",\n\t\t\t\t\"1763640000_m0.4.6.go\",\n\t\t\t}\n\t\t\tfor _, name := range migrations {\n\t\t\t\tapp.DB().NewQuery(\"DELETE FROM _migrations WHERE file='\" + name + \"'\").Execute()\n\t\t\t}\n\t\t}\n\n\t\ttracer.Printf(\"done\")\n\t\treturn nil\n\t}, func(app core.App) error {\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "migrations/1766800800_upgrade_v0.4.12.go",
    "content": "package migrations\n\nimport (\n\t\"strings\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\tm \"github.com/pocketbase/pocketbase/migrations\"\n\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcertx509 \"github.com/certimate-go/certimate/pkg/utils/cert/x509\"\n)\n\nfunc init() {\n\tm.Register(func(app core.App) error {\n\t\ttracer := NewTracer(\"v0.4.12\")\n\t\ttracer.Printf(\"go ...\")\n\n\t\t// update collection `certificate`\n\t\t//   - update field `subjectAltNames`\n\t\t{\n\t\t\tcollection, err := app.FindCollectionByNameOrId(\"4szxr9x43tpj6np\")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfor _, record := range records {\n\t\t\t\tchanged := false\n\n\t\t\t\tif certX509, err := xcert.ParseCertificateFromPEM(record.GetString(\"certificate\")); err == nil {\n\t\t\t\t\tcertSANs := xcertx509.GetSubjectAltNames(certX509)\n\t\t\t\t\tif strings.Join(certSANs, \";\") != record.GetString(\"subjectAltNames\") {\n\t\t\t\t\t\trecord.Set(\"subjectAltNames\", strings.Join(certSANs, \";\"))\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif changed {\n\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttracer.Printf(\"done\")\n\t\treturn nil\n\t}, func(app core.App) error {\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "migrations/1767024000_upgrade_v0.4.13.go",
    "content": "package migrations\n\nimport (\n\t\"github.com/pocketbase/pocketbase/core\"\n\tm \"github.com/pocketbase/pocketbase/migrations\"\n\n\tsnaps \"github.com/certimate-go/certimate/migrations/snaps/v0.4\"\n)\n\nfunc init() {\n\tm.Register(func(app core.App) error {\n\t\ttracer := NewTracer(\"v0.4.13\")\n\t\ttracer.Printf(\"go ...\")\n\n\t\t// adapt to new workflow data structure\n\t\t{\n\t\t\twalker := &snaps.WorkflowGraphWalker{}\n\t\t\twalker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) {\n\t\t\t\t_changed = false\n\t\t\t\t_err = nil\n\n\t\t\t\tif node.Type != \"bizDeploy\" {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tnodeCfg := node.Data.Config\n\n\t\t\t\tswitch nodeCfg[\"provider\"] {\n\t\t\t\tcase \"1panel-site\":\n\t\t\t\t\t{\n\t\t\t\t\t\tnodeCfg[\"provider\"] = \"1panel\"\n\n\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\tcase \"baotapanel-site\":\n\t\t\t\t\t{\n\t\t\t\t\t\tnodeCfg[\"provider\"] = \"baotapanel\"\n\n\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\tcase \"baotapanelgo-site\":\n\t\t\t\t\t{\n\t\t\t\t\t\tnodeCfg[\"provider\"] = \"baotapanelgo\"\n\n\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\tcase \"baotawaf-site\":\n\t\t\t\t\t{\n\t\t\t\t\t\tnodeCfg[\"provider\"] = \"baotawaf\"\n\n\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\tcase \"cdnfly\":\n\t\t\t\t\t{\n\t\t\t\t\t\tif providerCfg, ok := nodeCfg[\"providerConfig\"].(map[string]any); ok {\n\t\t\t\t\t\t\tif providerCfg[\"resourceType\"] == \"site\" {\n\t\t\t\t\t\t\t\tproviderCfg[\"resourceType\"] = \"website\"\n\t\t\t\t\t\t\t\tnodeCfg[\"providerConfig\"] = providerCfg\n\n\t\t\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\tcase \"cpanel-site\":\n\t\t\t\t\t{\n\t\t\t\t\t\tif providerCfg, ok := nodeCfg[\"providerConfig\"].(map[string]any); ok {\n\t\t\t\t\t\t\tproviderCfg[\"resourceType\"] = \"website\"\n\t\t\t\t\t\t\tnodeCfg[\"providerConfig\"] = providerCfg\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tnodeCfg[\"provider\"] = \"cpanel\"\n\n\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\tcase \"netlify-site\":\n\t\t\t\t\t{\n\t\t\t\t\t\tif providerCfg, ok := nodeCfg[\"providerConfig\"].(map[string]any); ok {\n\t\t\t\t\t\t\tproviderCfg[\"resourceType\"] = \"website\"\n\t\t\t\t\t\t\tnodeCfg[\"providerConfig\"] = providerCfg\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tnodeCfg[\"provider\"] = \"netlify\"\n\n\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\tcase \"ratpanel-site\":\n\t\t\t\t\t{\n\t\t\t\t\t\tif providerCfg, ok := nodeCfg[\"providerConfig\"].(map[string]any); ok {\n\t\t\t\t\t\t\tproviderCfg[\"resourceType\"] = \"website\"\n\t\t\t\t\t\t\tnodeCfg[\"providerConfig\"] = providerCfg\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tnodeCfg[\"provider\"] = \"ratpanel\"\n\n\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\tcase \"safeline-site\":\n\t\t\t\t\t{\n\t\t\t\t\t\tnodeCfg[\"provider\"] = \"safeline\"\n\n\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t})\n\n\t\t\t// update collection `workflow`\n\t\t\t//   - migrate field `graphDraft` / `graphContent`\n\t\t\t{\n\t\t\t\tcollection, err := app.FindCollectionByNameOrId(\"tovyif5ax6j62ur\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfor _, record := range records {\n\t\t\t\t\tchanged := false\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graphDraft\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graphContent\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif changed {\n\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// update collection `workflow_run`\n\t\t\t//   - migrate field `graph`\n\t\t\t{\n\t\t\t\tcollection, err := app.FindCollectionByNameOrId(\"qjp8lygssgwyqyz\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfor _, record := range records {\n\t\t\t\t\tchanged := false\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graph\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif changed {\n\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttracer.Printf(\"done\")\n\t\treturn nil\n\t}, func(app core.App) error {\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "migrations/1768363200_upgrade_v0.4.14.go",
    "content": "package migrations\n\nimport (\n\t\"github.com/pocketbase/pocketbase/core\"\n\tm \"github.com/pocketbase/pocketbase/migrations\"\n)\n\nfunc init() {\n\tm.Register(func(app core.App) error {\n\t\ttracer := NewTracer(\"v0.4.14\")\n\t\ttracer.Printf(\"go ...\")\n\n\t\t// update collection `settings`\n\t\t//   - modify field `content` schema of `sslProvider`\n\t\t{\n\t\t\tcollection, err := app.FindCollectionByNameOrId(\"dy6ccjb60spfy6p\")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\trecords, err := app.FindRecordsByFilter(collection, \"name=\\\"sslProvider\\\"\", \"\", 1, 0)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t} else if len(records) != 0 {\n\t\t\t\trecord := records[0]\n\t\t\t\tchanged := false\n\n\t\t\t\tcontent := make(map[string]any)\n\t\t\t\tif err := record.UnmarshalJSONField(\"content\", &content); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t} else {\n\t\t\t\t\tif _, ok := content[\"config\"]; ok {\n\t\t\t\t\t\tcontent[\"configs\"] = content[\"config\"]\n\t\t\t\t\t\tdelete(content, \"config\")\n\n\t\t\t\t\t\trecord.Set(\"content\", content)\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif changed {\n\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttracer.Printf(\"done\")\n\t\treturn nil\n\t}, func(app core.App) error {\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "migrations/1769313600_upgrade_v0.4.15.go",
    "content": "package migrations\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\n\t\"github.com/go-viper/mapstructure/v2\"\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\tm \"github.com/pocketbase/pocketbase/migrations\"\n\n\tsnaps \"github.com/certimate-go/certimate/migrations/snaps/v0.4\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcertx509 \"github.com/certimate-go/certimate/pkg/utils/cert/x509\"\n)\n\nfunc init() {\n\tm.Register(func(app core.App) error {\n\t\ttracer := NewTracer(\"v0.4.15\")\n\t\ttracer.Printf(\"go ...\")\n\n\t\t// update collection `acme_accounts`\n\t\t//   - rebuild indexes\n\t\t{\n\t\t\tcollection, err := app.FindCollectionByNameOrId(\"012d7abbod1hwvr\")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := json.Unmarshal([]byte(`{\n\t\t\t\t\"indexes\": [\n\t\t\t\t\t\"CREATE INDEX `+\"`\"+`idx_dQiYzimY7m`+\"`\"+` ON `+\"`\"+`acme_accounts`+\"`\"+` (`+\"`\"+`ca`+\"`\"+`)\",\n\t\t\t\t\t\"CREATE INDEX `+\"`\"+`idx_TjyqY6LAGa`+\"`\"+` ON `+\"`\"+`acme_accounts`+\"`\"+` (\\n  `+\"`\"+`ca`+\"`\"+`,\\n  `+\"`\"+`acmeDirUrl`+\"`\"+`\\n)\",\n\t\t\t\t\t\"CREATE UNIQUE INDEX `+\"`\"+`idx_G4brUDgxzc`+\"`\"+` ON `+\"`\"+`acme_accounts`+\"`\"+` (\\n  `+\"`\"+`ca`+\"`\"+`,\\n  `+\"`\"+`email`+\"`\"+`,\\n  `+\"`\"+`acmeAcctUrl`+\"`\"+`,\\n  `+\"`\"+`acmeDirUrl`+\"`\"+`\\n)\"\n\t\t\t\t]\n\t\t\t}`), &collection); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := app.Save(collection); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\ttracer.Printf(\"collection '%s' updated\", collection.Name)\n\t\t}\n\n\t\t// update collection `certificate`\n\t\t//   - update field `subjectAltNames`\n\t\t//   - remove field `acmeCertStableUrl`\n\t\t{\n\t\t\tcollection, err := app.FindCollectionByNameOrId(\"4szxr9x43tpj6np\")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tcollection.Fields.RemoveByName(\"acmeCertStableUrl\")\n\n\t\t\tif err := app.Save(collection); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\ttracer.Printf(\"collection '%s' updated\", collection.Name)\n\n\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfor _, record := range records {\n\t\t\t\tchanged := false\n\n\t\t\t\tif certX509, err := xcert.ParseCertificateFromPEM(record.GetString(\"certificate\")); err == nil {\n\t\t\t\t\tcertSANs := xcertx509.GetSubjectAltNames(certX509)\n\t\t\t\t\tif strings.Join(certSANs, \";\") != record.GetString(\"subjectAltNames\") {\n\t\t\t\t\t\trecord.Set(\"subjectAltNames\", strings.Join(certSANs, \";\"))\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif changed {\n\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// update collection `workflow_output`\n\t\t//   - revert data for #1137\n\t\t{\n\t\t\tcollection, err := app.FindCollectionByNameOrId(\"bqnxb95f2cooowp\")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfor _, record := range records {\n\t\t\t\tchanged := false\n\n\t\t\t\trunRecord, _ := app.FindFirstRecordByFilter(\"workflow_run\", \"id={:runId}\", dbx.Params{\"runId\": record.GetString(\"runRef\")})\n\t\t\t\tif runRecord != nil {\n\t\t\t\t\trunGraph := make(map[string]any)\n\t\t\t\t\tif err := runRecord.UnmarshalJSONField(\"graph\", &runGraph); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tif _, ok := runGraph[\"nodes\"]; ok {\n\t\t\t\t\t\tnodes := make([]*snaps.WorkflowNode, 0)\n\t\t\t\t\t\tif err := mapstructure.Decode(runGraph[\"nodes\"], &nodes); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tnodeMaybeBrokenId := record.GetString(\"nodeId\")\n\n\t\t\t\t\t\tvar findNode func(blocks []*snaps.WorkflowNode) *snaps.WorkflowNode\n\t\t\t\t\t\tfindNode = func(blocks []*snaps.WorkflowNode) *snaps.WorkflowNode {\n\t\t\t\t\t\t\tfor _, node := range blocks {\n\t\t\t\t\t\t\t\tif node.Id == nodeMaybeBrokenId {\n\t\t\t\t\t\t\t\t\treturn node\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif len(node.Blocks) > 0 {\n\t\t\t\t\t\t\t\t\tif node := findNode(node.Blocks); node != nil {\n\t\t\t\t\t\t\t\t\t\treturn node\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif node := findNode(nodes); node != nil {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tvar findNodeEx func(blocks []*snaps.WorkflowNode) *snaps.WorkflowNode\n\t\t\t\t\t\tfindNodeEx = func(blocks []*snaps.WorkflowNode) *snaps.WorkflowNode {\n\t\t\t\t\t\t\tfor _, node := range blocks {\n\t\t\t\t\t\t\t\tconst TRUNCATED_LENGTH = 3 // same as `ATTEMPTS` in '1757476800_upgrade_v0.4.0.go'\n\t\t\t\t\t\t\t\tif strings.HasSuffix(node.Id, nodeMaybeBrokenId) && (len(node.Id)-len(nodeMaybeBrokenId) == TRUNCATED_LENGTH) {\n\t\t\t\t\t\t\t\t\treturn node\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif len(node.Blocks) > 0 {\n\t\t\t\t\t\t\t\t\tif node := findNodeEx(node.Blocks); node != nil {\n\t\t\t\t\t\t\t\t\t\treturn node\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif node := findNodeEx(nodes); node != nil {\n\t\t\t\t\t\t\trecord.Set(\"nodeId\", node.Id)\n\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif changed {\n\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// adapt to new workflow data structure\n\t\t{\n\t\t\twalker := &snaps.WorkflowGraphWalker{}\n\t\t\twalker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) {\n\t\t\t\t_changed = false\n\t\t\t\t_err = nil\n\n\t\t\t\tif node.Type != \"bizDeploy\" {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tnodeCfg := node.Data.Config\n\n\t\t\t\tif nodeCfg[\"provider\"] == \"rainyun-rcdn\" {\n\t\t\t\t\tif providerCfg, ok := nodeCfg[\"providerConfig\"].(map[string]any); ok {\n\t\t\t\t\t\tif providerCfg[\"resourceType\"] == \"certificate\" {\n\t\t\t\t\t\t\tdelete(providerCfg, \"resourceType\")\n\t\t\t\t\t\t\tdelete(providerCfg, \"instanceId\")\n\t\t\t\t\t\t\tdelete(providerCfg, \"domainMatchPattern\")\n\t\t\t\t\t\t\tdelete(providerCfg, \"domain\")\n\t\t\t\t\t\t\tnodeCfg[\"provider\"] = \"rainyun-sslcenter\"\n\t\t\t\t\t\t\tnodeCfg[\"providerConfig\"] = providerCfg\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tdelete(providerCfg, \"resourceType\")\n\t\t\t\t\t\t\tdelete(providerCfg, \"certificateId\")\n\t\t\t\t\t\t\tnodeCfg[\"providerConfig\"] = providerCfg\n\t\t\t\t\t\t}\n\t\t\t\t\t\t_changed = true\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t})\n\n\t\t\t// update collection `workflow`\n\t\t\t//   - migrate field `graphDraft` / `graphContent`\n\t\t\t{\n\t\t\t\tcollection, err := app.FindCollectionByNameOrId(\"tovyif5ax6j62ur\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfor _, record := range records {\n\t\t\t\t\tchanged := false\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graphDraft\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graphContent\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif changed {\n\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// update collection `workflow_run`\n\t\t\t//   - migrate field `graph`\n\t\t\t{\n\t\t\t\tcollection, err := app.FindCollectionByNameOrId(\"qjp8lygssgwyqyz\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trecords, err := app.FindAllRecords(collection)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tfor _, record := range records {\n\t\t\t\t\tchanged := false\n\n\t\t\t\t\tif ret, err := walker.Migrate(record, \"graph\"); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchanged = changed || ret\n\t\t\t\t\t}\n\n\t\t\t\t\tif changed {\n\t\t\t\t\t\tif err := app.Save(record); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttracer.Printf(\"record #%s in collection '%s' updated\", record.Id, collection.Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttracer.Printf(\"done\")\n\t\treturn nil\n\t}, func(app core.App) error {\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "migrations/snaps/v0.3/workflow.go",
    "content": "﻿package snaps\n\n// This is a definition backup of WorkflowNode for v0.3.\ntype WorkflowNode struct {\n\tId       string          `json:\"id\"`\n\tType     string          `json:\"type\"`\n\tName     string          `json:\"name\"`\n\tConfig   map[string]any  `json:\"config,omitempty\"`\n\tNext     *WorkflowNode   `json:\"next,omitempty\"`\n\tBranches []*WorkflowNode `json:\"branches,omitempty\"`\n}\n"
  },
  {
    "path": "migrations/snaps/v0.4/workflow.go",
    "content": "package snaps\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-viper/mapstructure/v2\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\ntype WorkflowGraphWalker struct {\n\tvisitors []WorkflowNodeVisitor\n}\n\ntype WorkflowNodeVisitor func(node *WorkflowNode) (_changed bool, _err error)\n\nfunc (w *WorkflowGraphWalker) Define(visitor WorkflowNodeVisitor) {\n\tif w.visitors == nil {\n\t\tw.visitors = make([]WorkflowNodeVisitor, 0)\n\t}\n\tw.visitors = append(w.visitors, visitor)\n}\n\nfunc (w *WorkflowGraphWalker) Visit(nodes []*WorkflowNode) (_changed bool, _err error) {\n\tchanged := false\n\n\tif w.visitors == nil {\n\t\treturn changed, nil\n\t}\n\n\tfor _, node := range nodes {\n\t\tfor _, visitor := range w.visitors {\n\t\t\tnodeChanged, err := visitor(node)\n\t\t\tif err != nil {\n\t\t\t\treturn changed, err\n\t\t\t}\n\t\t\tif nodeChanged {\n\t\t\t\tchanged = true\n\t\t\t}\n\n\t\t\tif len(node.Blocks) > 0 {\n\t\t\t\tblocksChanged, err := w.Visit(node.Blocks)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn changed, err\n\t\t\t\t}\n\t\t\t\tif blocksChanged {\n\t\t\t\t\tchanged = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn changed, nil\n}\n\nfunc (w *WorkflowGraphWalker) Migrate(record *core.Record, field string) (_changed bool, _err error) {\n\tf := record.Collection().Fields.GetByName(field)\n\tif f == nil {\n\t\treturn false, fmt.Errorf(\"field '%s' not found\", field)\n\t}\n\n\tif record.GetRaw(field) != nil {\n\t\tgraph := make(map[string]any)\n\t\tif err := record.UnmarshalJSONField(field, &graph); err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tif _, ok := graph[\"nodes\"]; ok {\n\t\t\tnodes := make([]*WorkflowNode, 0)\n\t\t\tif err := mapstructure.Decode(graph[\"nodes\"], &nodes); err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\n\t\t\tnodesChanged, err := w.Visit(nodes)\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t} else if nodesChanged {\n\t\t\t\tgraph[\"nodes\"] = nodes\n\t\t\t\trecord.Set(field, graph)\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\n// This is a definition copy of WorkflowNode.\n// see: /internal/domain/workflow.go\ntype WorkflowNode struct {\n\tId     string             `json:\"id\"`\n\tType   string             `json:\"type\"`\n\tData   WorkflowNodeData   `json:\"data\"`\n\tBlocks WorkflowNodeBlocks `json:\"blocks,omitempty,omitzero\"`\n}\n\n// This is a definition copy of []*WorkflowNode.\n// see: /internal/domain/workflow.go\ntype WorkflowNodeBlocks []*WorkflowNode\n\n// This is a definition copy of WorkflowNodeData.\n// see: /internal/domain/workflow.go\ntype WorkflowNodeData struct {\n\tName     string             `json:\"name\"`\n\tDisabled bool               `json:\"disabled,omitempty,omitzero\"`\n\tConfig   WorkflowNodeConfig `json:\"config,omitempty,omitzero\"`\n}\n\n// This is a definition copy of WorkflowNodeConfig.\n// see: /internal/domain/workflow.go\ntype WorkflowNodeConfig map[string]any\n\nfunc (g WorkflowNodeBlocks) GetNodeById(nodeId string) (*WorkflowNode, bool) {\n\treturn g.getNodeInBlocksById(g, nodeId)\n}\n\nfunc (g WorkflowNodeBlocks) getNodeInBlocksById(blocks WorkflowNodeBlocks, nodeId string) (*WorkflowNode, bool) {\n\tfor _, node := range blocks {\n\t\tif node.Id == nodeId {\n\t\t\treturn node, true\n\t\t}\n\n\t\tif len(node.Blocks) > 0 {\n\t\t\tif found, ok := g.getNodeInBlocksById(node.Blocks, nodeId); ok {\n\t\t\t\treturn found, true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, false\n}\n"
  },
  {
    "path": "migrations/tracer.go",
    "content": "package migrations\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n)\n\ntype Tracer struct {\n\tlogger *slog.Logger\n\tflag   string\n}\n\nfunc NewTracer(flag string) *Tracer {\n\treturn &Tracer{\n\t\tlogger: slog.Default(),\n\t\tflag:   flag,\n\t}\n}\n\nfunc (l *Tracer) Printf(format string, args ...any) {\n\tl.logger.Info(\"[CERTIMATE] migration \" + l.flag + \": \" + fmt.Sprintf(format, args...))\n}\n"
  },
  {
    "path": "pkg/core/certifier/challenger.go",
    "content": "package certifier\n\nimport (\n\t\"github.com/go-acme/lego/v4/challenge\"\n)\n\ntype ACMEChallenger = challenge.Provider\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/35cn/35cn.go",
    "content": "package west35cn\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/com35\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tUsername              string `json:\"username\"`\n\tApiPassword           string `json:\"apiPassword\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := com35.NewDefaultConfig()\n\tproviderConfig.Username = config.Username\n\tproviderConfig.Password = config.ApiPassword\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := com35.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/51dnscom/51dnscom.go",
    "content": "package dnscom\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/51dnscom/internal\"\n)\n\ntype ChallengerConfig struct {\n\tApiKey                string `json:\"apiKey\"`\n\tApiSecret             string `json:\"apiSecret\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := internal.NewDefaultConfig()\n\tproviderConfig.APIKey = config.ApiKey\n\tproviderConfig.APISecret = config.ApiSecret\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := internal.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/51dnscom/internal/lego.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/samber/lo\"\n\n\tdnscomsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/51dnscom\"\n)\n\nconst (\n\tenvNamespace = \"51DNSCOM_\"\n\tEnvAPIKey    = envNamespace + \"API_KEY\"\n\tEnvAPISecret = envNamespace + \"API_SECRET\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\ntype Config struct {\n\tAPIKey    string\n\tAPISecret string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        time.Duration\n}\n\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *dnscomsdk.Client\n\n\trecordCache   map[string]dnsRecordCacheEntry // Key: ChallengeToken\n\trecordCacheMu sync.Mutex\n}\n\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t}\n}\n\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey, EnvAPISecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"51dnscom: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.APISecret = values[EnvAPISecret]\n\n\treturn NewDNSProviderConfig(config)\n}\n\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"51dnscom: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := dnscomsdk.NewClient(config.APIKey, config.APISecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"51dnscom: %w\", err)\n\t} else {\n\t\tclient.SetTimeout(config.HTTPTimeout)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:        config,\n\t\tclient:        client,\n\t\trecordCache:   make(map[string]dnsRecordCacheEntry),\n\t\trecordCacheMu: sync.Mutex{},\n\t}, nil\n}\n\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"51dnscom: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"51dnscom: %w\", err)\n\t}\n\n\tzone, err := d.findZone(dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"51dnscom: error when list zones: %w\", err)\n\t}\n\n\t// REF: https://www.51dns.com/document/api/4/12.html\n\trequest := &dnscomsdk.RecordCreateRequest{\n\t\tDomainID: lo.ToPtr(zone.DomainID.String()),\n\t\tType:     lo.ToPtr(\"TXT\"),\n\t\tHost:     lo.ToPtr(subDomain),\n\t\tValue:    lo.ToPtr(info.Value),\n\t\tTTL:      lo.ToPtr(int32(d.config.TTL)),\n\t}\n\tresponse, err := d.client.RecordCreate(request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"51dnscom: error when create record: %w\", err)\n\t}\n\n\td.recordCacheMu.Lock()\n\td.recordCache[token] = dnsRecordCacheEntry{DomainID: zone.DomainID.String(), RecordID: response.Data.RecordID.String()}\n\td.recordCacheMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordCacheMu.Lock()\n\trecord, ok := d.recordCache[token]\n\td.recordCacheMu.Unlock()\n\tif !ok {\n\t\treturn fmt.Errorf(\"51dnscom: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\t// REF: https://www.51dns.com/document/api/4/27.html\n\trequest := &dnscomsdk.RecordRemoveRequest{\n\t\tDomainID: lo.ToPtr(record.DomainID),\n\t\tRecordID: lo.ToPtr(record.RecordID),\n\t}\n\tif _, err := d.client.RecordRemove(request); err != nil {\n\t\treturn fmt.Errorf(\"51dnscom: error when delete record: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\ntype dnsRecordCacheEntry struct {\n\tDomainID string\n\tRecordID string\n}\n\nfunc (d *DNSProvider) findZone(zoneName string) (*dnscomsdk.DomainRecord, error) {\n\tpage := 1\n\tpageSize := 10\n\tfor {\n\t\t// REF: https://www.51dns.com/document/api/74/88.html\n\t\trequest := &dnscomsdk.DomainListRequest{\n\t\t\tPage:     lo.ToPtr(int32(page)),\n\t\t\tPageSize: lo.ToPtr(int32(pageSize)),\n\t\t}\n\t\tresponse, err := d.client.DomainList(request)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif response.Data == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, domainItem := range response.Data.Data {\n\t\t\tif domainItem.Domain == zoneName {\n\t\t\t\treturn domainItem, nil\n\t\t\t}\n\t\t}\n\n\t\tif len(response.Data.Data) < pageSize || response.Data.PageCount <= int32(page) {\n\t\t\tbreak\n\t\t}\n\n\t\tpage++\n\t}\n\n\treturn nil, fmt.Errorf(\"could not find zone '%s'\", zoneName)\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/acmedns/acmedns.go",
    "content": "package acmedns\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/acmedns\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tServerUrl   string `json:\"serverUrl\"`\n\tCredentials string `json:\"credentials\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\ttempfile, err := os.CreateTemp(\"\", \"certimate.acmedns_*.tmp\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create temp credentials file: %w\", err)\n\t} else {\n\t\tif _, err := tempfile.Write([]byte(config.Credentials)); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to write temp credentials file: %w\", err)\n\t\t}\n\n\t\ttempfile.Close()\n\t}\n\n\tproviderConfig := acmedns.NewDefaultConfig()\n\tproviderConfig.APIBase = config.ServerUrl\n\tproviderConfig.StoragePath = tempfile.Name()\n\n\tprovider, err := acmedns.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/acmehttpreq/acmehttpreq.go",
    "content": "package acmehttpreq\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/httpreq\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tEndpoint              string `json:\"endpoint\"`\n\tMode                  string `json:\"mode\"`\n\tUsername              string `json:\"username\"`\n\tPassword              string `json:\"password\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tendpoint, _ := url.Parse(config.Endpoint)\n\tproviderConfig := httpreq.NewDefaultConfig()\n\tproviderConfig.Endpoint = endpoint\n\tproviderConfig.Mode = config.Mode\n\tproviderConfig.Username = config.Username\n\tproviderConfig.Password = config.Password\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\n\tprovider, err := httpreq.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/akamai-edgedns/akamai_edgedns.go",
    "content": "package akamaiedgedns\n\nimport (\n\t\"time\"\n\n\t\"github.com/akamai/AkamaiOPEN-edgegrid-golang/v11/pkg/edgegrid\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/providers/dns/edgedns\"\n)\n\ntype ChallengerConfig struct {\n\tHost                  string\n\tClientToken           string\n\tClientSecret          string\n\tAccessToken           string\n\tDnsPropagationTimeout int\n\tDnsTTL                int\n}\n\nfunc NewChallenger(config *ChallengerConfig) (challenge.Provider, error) {\n\tedgegridConfig := &edgegrid.Config{\n\t\tHost:         config.Host,\n\t\tClientToken:  config.ClientToken,\n\t\tClientSecret: config.ClientSecret,\n\t\tAccessToken:  config.AccessToken,\n\t\tMaxBody:      131072,\n\t\tHeaderToSign: []string{\n\t\t\t\"X-Akamai-ACS-Action\",\n\t\t\t\"X-Akamai-ACS-Auth-Data\",\n\t\t\t\"X-Akamai-ACS-Auth-Sign\",\n\t\t},\n\t}\n\n\tproviderConfig := edgedns.NewDefaultConfig()\n\tproviderConfig.Config = edgegridConfig\n\tif config.DnsPropagationTimeout > 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL > 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := edgedns.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/aliyun/aliyun.go",
    "content": "package aliyun\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/alidns\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tAccessKeyId           string `json:\"accessKeyId\"`\n\tAccessKeySecret       string `json:\"accessKeySecret\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := alidns.NewDefaultConfig()\n\tproviderConfig.APIKey = config.AccessKeyId\n\tproviderConfig.SecretKey = config.AccessKeySecret\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := alidns.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/aliyun-esa/aliyun_esa.go",
    "content": "package aliyunesa\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/aliesa\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tAccessKeyId           string `json:\"accessKeyId\"`\n\tAccessKeySecret       string `json:\"accessKeySecret\"`\n\tRegion                string `json:\"region\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := aliesa.NewDefaultConfig()\n\tproviderConfig.APIKey = config.AccessKeyId\n\tproviderConfig.SecretKey = config.AccessKeySecret\n\tproviderConfig.RegionID = config.Region\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := aliesa.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/arvancloud/arvancloud.go",
    "content": "package arvancloud\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/arvancloud\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiKey                string `json:\"apiKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := arvancloud.NewDefaultConfig()\n\tproviderConfig.APIKey = config.ApiKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := arvancloud.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/aws-route53/aws-route53.go",
    "content": "package awsroute53\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/route53\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tAccessKeyId           string `json:\"accessKeyId\"`\n\tSecretAccessKey       string `json:\"secretAccessKey\"`\n\tRegion                string `json:\"region\"`\n\tHostedZoneId          string `json:\"hostedZoneId,omitempty\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := route53.NewDefaultConfig()\n\tproviderConfig.AccessKeyID = config.AccessKeyId\n\tproviderConfig.SecretAccessKey = config.SecretAccessKey\n\tproviderConfig.Region = config.Region\n\tif config.HostedZoneId != \"\" {\n\t\tproviderConfig.HostedZoneID = config.HostedZoneId\n\t}\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := route53.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/azure-dns/azure-dns.go",
    "content": "package azuredns\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/azuredns\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n\tazenv \"github.com/certimate-go/certimate/pkg/sdk3rd/azure/env\"\n)\n\ntype ChallengerConfig struct {\n\tTenantId              string `json:\"tenantId\"`\n\tClientId              string `json:\"clientId\"`\n\tClientSecret          string `json:\"clientSecret\"`\n\tSubscriptionId        string `json:\"subscriptionId,omitempty\"`\n\tResourceGroupName     string `json:\"resourceGroupName,omitempty\"`\n\tCloudName             string `json:\"cloudName,omitempty\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := azuredns.NewDefaultConfig()\n\tproviderConfig.AuthMethod = \"env\"\n\tproviderConfig.TenantID = config.TenantId\n\tproviderConfig.ClientID = config.ClientId\n\tproviderConfig.ClientSecret = config.ClientSecret\n\tproviderConfig.SubscriptionID = config.SubscriptionId\n\tproviderConfig.ResourceGroup = config.ResourceGroupName\n\tif config.CloudName != \"\" {\n\t\tenv, err := azenv.GetCloudEnvConfiguration(config.CloudName)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tproviderConfig.Environment = env\n\t}\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := azuredns.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/baiducloud/baiducloud.go",
    "content": "package baiducloud\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/baiducloud\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tAccessKeyId           string `json:\"accessKeyId\"`\n\tSecretAccessKey       string `json:\"secretAccessKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := baiducloud.NewDefaultConfig()\n\tproviderConfig.AccessKeyID = config.AccessKeyId\n\tproviderConfig.SecretAccessKey = config.SecretAccessKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := baiducloud.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/bookmyname/bookmyname.go",
    "content": "package bookmyname\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/bookmyname\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tUsername              string `json:\"username\"`\n\tPassword              string `json:\"password\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := bookmyname.NewDefaultConfig()\n\tproviderConfig.Username = config.Username\n\tproviderConfig.Password = config.Password\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := bookmyname.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/bunny/bunny.go",
    "content": "package bunny\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/bunny\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiKey                string `json:\"apiKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := bunny.NewDefaultConfig()\n\tproviderConfig.APIKey = config.ApiKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := bunny.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/cloudflare/cloudflare.go",
    "content": "package cloudflare\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/cloudflare\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tDnsApiToken           string `json:\"dnsApiToken\"`\n\tZoneApiToken          string `json:\"zoneApiToken,omitempty\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := cloudflare.NewDefaultConfig()\n\tproviderConfig.AuthToken = config.DnsApiToken\n\tproviderConfig.ZoneToken = config.ZoneApiToken\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := cloudflare.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/cloudns/cloudns.go",
    "content": "package cloudns\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/cloudns\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tAuthId                string `json:\"authId\"`\n\tAuthPassword          string `json:\"authPassword\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := cloudns.NewDefaultConfig()\n\tproviderConfig.AuthID = config.AuthId\n\tproviderConfig.AuthPassword = config.AuthPassword\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := cloudns.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/cmcccloud/cmcccloud.go",
    "content": "package cmcccloud\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/cmcccloud/internal\"\n)\n\ntype ChallengerConfig struct {\n\tAccessKeyId           string `json:\"accessKeyId\"`\n\tAccessKeySecret       string `json:\"accessKeySecret\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := internal.NewDefaultConfig()\n\tproviderConfig.AccessKey = config.AccessKeyId\n\tproviderConfig.SecretKey = config.AccessKeySecret\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\n\tprovider, err := internal.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/cmcccloud/internal/lego.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/samber/lo\"\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkclouddns\"\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkclouddns/model\"\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkcore/config\"\n)\n\nconst (\n\tenvNamespace = \"CMCCCLOUD_\"\n\n\tEnvAccessKey = envNamespace + \"ACCESS_KEY\"\n\tEnvSecretKey = envNamespace + \"SECRET_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvReadTimeout        = envNamespace + \"READ_TIMEOUT\"\n\tEnvConnectTimeout     = envNamespace + \"CONNECT_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\ntype Config struct {\n\tAccessKey string\n\tSecretKey string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tReadTimeout        int\n\tConnectTimeout     int\n}\n\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *ecloudsdkclouddns.Client\n\n\trecordIDs   map[string]string // Key: ChallengeToken; Value: RecordID\n\trecordIDsMu sync.Mutex\n}\n\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tReadTimeout:        env.GetOrDefaultInt(EnvReadTimeout, 30),\n\t\tConnectTimeout:     env.GetOrDefaultInt(EnvConnectTimeout, 30),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t}\n}\n\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAccessKey, EnvSecretKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cmccecloud: %w\", err)\n\t}\n\n\tcfg := NewDefaultConfig()\n\tcfg.AccessKey = values[EnvAccessKey]\n\tcfg.SecretKey = values[EnvSecretKey]\n\n\treturn NewDNSProviderConfig(cfg)\n}\n\nfunc NewDNSProviderConfig(cfg *Config) (*DNSProvider, error) {\n\tif cfg == nil {\n\t\treturn nil, errors.New(\"cmccecloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient := ecloudsdkclouddns.NewClient(&config.Config{\n\t\tAccessKey: cfg.AccessKey,\n\t\tSecretKey: cfg.SecretKey,\n\t\t// 资源池常量见: https://ecloud.10086.cn/op-help-center/doc/article/54462\n\t\t// 默认全局\n\t\tPoolId:         \"CIDC-CORE-00\",\n\t\tReadTimeOut:    cfg.ReadTimeout,\n\t\tConnectTimeout: cfg.ConnectTimeout,\n\t})\n\n\treturn &DNSProvider{\n\t\tconfig:      cfg,\n\t\tclient:      client,\n\t\trecordIDs:   make(map[string]string),\n\t\trecordIDsMu: sync.Mutex{},\n\t}, nil\n}\n\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzoneName, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cmccecloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zoneName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cmccecloud: %w\", err)\n\t}\n\n\trequest := &model.CreateRecordOpenapiRequest{\n\t\tCreateRecordOpenapiBody: &model.CreateRecordOpenapiBody{\n\t\t\tLineId:      \"0\", // 默认线路\n\t\t\tRr:          subDomain,\n\t\t\tDomainName:  dns01.UnFqdn(zoneName),\n\t\t\tDescription: \"certimate acme\",\n\t\t\tType:        model.CreateRecordOpenapiBodyTypeEnumTxt,\n\t\t\tValue:       info.Value,\n\t\t\tTtl:         lo.ToPtr(int32(d.config.TTL)),\n\t\t},\n\t}\n\tresponse, err := d.client.CreateRecordOpenapi(request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cmccecloud: error when create record: %w\", err)\n\t} else if response.State != model.CreateRecordOpenapiResponseStateEnumOk {\n\t\treturn fmt.Errorf(\"cmccecloud: failed to create record: unexpected response state: '%s', errcode: '%s', errmsg: '%s'\", response.State, response.ErrorCode, response.ErrorMessage)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = response.Body.RecordId\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\tif !ok {\n\t\treturn fmt.Errorf(\"cmccecloud: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\trequest := &model.DeleteRecordOpenapiRequest{\n\t\tDeleteRecordOpenapiBody: &model.DeleteRecordOpenapiBody{\n\t\t\tRecordIdList: []string{recordID},\n\t\t},\n\t}\n\tresponse, err := d.client.DeleteRecordOpenapi(request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cmccecloud: error when delete record: %w\", err)\n\t} else if response.State != model.DeleteRecordOpenapiResponseStateEnumOk {\n\t\treturn fmt.Errorf(\"cmccecloud: failed to delete record, unexpected response state: '%s', errcode: '%s', errmsg: '%s'\", response.State, response.ErrorCode, response.ErrorMessage)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/constellix/constellix.go",
    "content": "package cloudns\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/constellix\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiKey                string `json:\"apiKey\"`\n\tSecretKey             string `json:\"secretKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := constellix.NewDefaultConfig()\n\tproviderConfig.APIKey = config.ApiKey\n\tproviderConfig.SecretKey = config.SecretKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := constellix.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/cpanel/cpanel.go",
    "content": "package cpanel\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/cpanel\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n\txhttp \"github.com/certimate-go/certimate/pkg/utils/http\"\n)\n\ntype ChallengerConfig struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tUsername                 string `json:\"username\"`\n\tApiToken                 string `json:\"apiToken\"`\n\tDnsPropagationTimeout    int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                   int    `json:\"dnsTTL,omitempty\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := cpanel.NewDefaultConfig()\n\tproviderConfig.Mode = \"cpanel\"\n\tproviderConfig.BaseURL = config.ServerUrl\n\tproviderConfig.Username = config.Username\n\tproviderConfig.Token = config.ApiToken\n\tif config.AllowInsecureConnections {\n\t\ttransport := xhttp.NewDefaultTransport()\n\t\ttransport.DisableKeepAlives = true\n\t\ttransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}\n\t\tproviderConfig.HTTPClient.Transport = transport\n\t}\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := cpanel.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/ctcccloud/ctcccloud.go",
    "content": "package ctcccloud\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/ctcccloud/internal\"\n)\n\ntype ChallengerConfig struct {\n\tAccessKeyId           string `json:\"accessKeyId\"`\n\tSecretAccessKey       string `json:\"secretAccessKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := internal.NewDefaultConfig()\n\tproviderConfig.AccessKeyId = config.AccessKeyId\n\tproviderConfig.SecretAccessKey = config.SecretAccessKey\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\n\tprovider, err := internal.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/ctcccloud/internal/lego.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/samber/lo\"\n\n\tctyundns \"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/dns\"\n)\n\nconst (\n\tenvNamespace = \"CTYUNSMARTDNS_\"\n\n\tEnvAccessKeyID     = envNamespace + \"ACCESS_KEY_ID\"\n\tEnvSecretAccessKey = envNamespace + \"SECRET_ACCESS_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\ntype Config struct {\n\tAccessKeyId     string\n\tSecretAccessKey string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        time.Duration\n}\n\ntype DNSProvider struct {\n\tclient *ctyundns.Client\n\tconfig *Config\n\n\trecordIDs   map[string]int32 // Key: ChallengeToken; Value: RecordID\n\trecordIDsMu sync.Mutex\n}\n\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t}\n}\n\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ctyun: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AccessKeyId = values[EnvAccessKeyID]\n\tconfig.SecretAccessKey = values[EnvSecretAccessKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"ctyun: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := ctyundns.NewClient(config.AccessKeyId, config.SecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ctyun: %w\", err)\n\t} else {\n\t\tclient.SetTimeout(config.HTTPTimeout)\n\t}\n\n\treturn &DNSProvider{\n\t\tclient:      client,\n\t\tconfig:      config,\n\t\trecordIDs:   make(map[string]int32),\n\t\trecordIDsMu: sync.Mutex{},\n\t}, nil\n}\n\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ctyun: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ctyun: %w\", err)\n\t}\n\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=122&api=11259&data=181&isNormal=1&vid=259\n\trequest := &ctyundns.AddRecordRequest{\n\t\tDomain:   lo.ToPtr(dns01.UnFqdn(authZone)),\n\t\tHost:     lo.ToPtr(subDomain),\n\t\tType:     lo.ToPtr(\"TXT\"),\n\t\tLineCode: lo.ToPtr(\"Default\"),\n\t\tValue:    lo.ToPtr(info.Value),\n\t\tState:    lo.ToPtr(int32(1)),\n\t\tTTL:      lo.ToPtr(int32(d.config.TTL)),\n\t}\n\tresponse, err := d.client.AddRecord(request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ctyun: error when create record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = response.ReturnObj.RecordId\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\tif !ok {\n\t\treturn fmt.Errorf(\"tencentcloud-eo: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=122&api=11262&data=181&isNormal=1&vid=259\n\trequest := &ctyundns.DeleteRecordRequest{\n\t\tRecordId: lo.ToPtr(recordID),\n\t}\n\tif _, err := d.client.DeleteRecord(request); err != nil {\n\t\treturn fmt.Errorf(\"ctyun: error when delete record: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/desec/desec.go",
    "content": "package desec\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/desec\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tToken                 string `json:\"token\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := desec.NewDefaultConfig()\n\tproviderConfig.Token = config.Token\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := desec.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/digitalocean/digitalocean.go",
    "content": "package namedotcom\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/digitalocean\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tAccessToken           string `json:\"accessToken\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := digitalocean.NewDefaultConfig()\n\tproviderConfig.AuthToken = config.AccessToken\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := digitalocean.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/dnsexit/dnsexit.go",
    "content": "package dnsexit\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/dnsexit\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiKey                string `json:\"apiKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := dnsexit.NewDefaultConfig()\n\tproviderConfig.APIKey = config.ApiKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := dnsexit.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/dnsla/dnsla.go",
    "content": "package dnsla\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dnsla/internal\"\n)\n\ntype ChallengerConfig struct {\n\tApiId                 string `json:\"apiId\"`\n\tApiSecret             string `json:\"apiSecret\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := internal.NewDefaultConfig()\n\tproviderConfig.APIId = config.ApiId\n\tproviderConfig.APISecret = config.ApiSecret\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := internal.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/dnsla/internal/lego.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/samber/lo\"\n\n\tdnslasdk \"github.com/certimate-go/certimate/pkg/sdk3rd/dnsla\"\n)\n\nconst (\n\tenvNamespace = \"DNSLA_\"\n\n\tEnvAPIId     = envNamespace + \"API_ID\"\n\tEnvAPISecret = envNamespace + \"API_SECRET\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\ntype Config struct {\n\tAPIId     string\n\tAPISecret string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        time.Duration\n}\n\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *dnslasdk.Client\n\n\trecordIDs   map[string]string // Key: ChallengeToken; Value: RecordID\n\trecordIDsMu sync.Mutex\n}\n\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t}\n}\n\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIId, EnvAPISecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dnsla: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIId = values[EnvAPIId]\n\tconfig.APISecret = values[EnvAPISecret]\n\n\treturn NewDNSProviderConfig(config)\n}\n\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"dnsla: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := dnslasdk.NewClient(config.APIId, config.APISecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dnsla: %w\", err)\n\t} else {\n\t\tclient.SetTimeout(config.HTTPTimeout)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:      config,\n\t\tclient:      client,\n\t\trecordIDs:   make(map[string]string),\n\t\trecordIDsMu: sync.Mutex{},\n\t}, nil\n}\n\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsla: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsla: %w\", err)\n\t}\n\n\tzone, err := d.findZone(dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsla: error when list zones: %w\", err)\n\t}\n\n\t// REF: https://www.dnsla.cn/docs/ApiDoc\n\trequest := &dnslasdk.CreateRecordRequest{\n\t\tDomainId: lo.ToPtr(zone.Id),\n\t\tType:     lo.ToPtr(int32(16)),\n\t\tHost:     lo.ToPtr(subDomain),\n\t\tData:     lo.ToPtr(info.Value),\n\t\tTtl:      lo.ToPtr(int32(d.config.TTL)),\n\t}\n\tresponse, err := d.client.CreateRecord(request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsla: error when create record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = response.Data.Id\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\tif !ok {\n\t\treturn fmt.Errorf(\"dnsla: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\t// REF: https://www.dnsla.cn/docs/ApiDoc\n\tif _, err := d.client.DeleteRecord(recordID); err != nil {\n\t\treturn fmt.Errorf(\"dnsla: error when delete record: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) findZone(zoneName string) (*dnslasdk.DomainRecord, error) {\n\tpageIndex := 1\n\tpageSize := 100\n\tfor {\n\t\t// REF: https://www.dnsla.cn/docs/ApiDoc\n\t\trequest := &dnslasdk.ListDomainsRequest{\n\t\t\tPageIndex: lo.ToPtr(int32(pageIndex)),\n\t\t\tPageSize:  lo.ToPtr(int32(pageSize)),\n\t\t}\n\t\tresponse, err := d.client.ListDomains(request)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif response.Data == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, domainItem := range response.Data.Results {\n\t\t\tif strings.TrimRight(domainItem.Domain, \".\") == zoneName || strings.TrimRight(domainItem.DisplayDomain, \".\") == zoneName {\n\t\t\t\treturn domainItem, nil\n\t\t\t}\n\t\t}\n\n\t\tif len(response.Data.Results) < pageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tpageIndex++\n\t}\n\n\treturn nil, fmt.Errorf(\"could not find zone '%s'\", zoneName)\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/dnsmadeeasy/dnsmadeeasy.go",
    "content": "package dnsmadeeasy\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiKey                string `json:\"apiKey\"`\n\tApiSecret             string `json:\"apiSecret\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := dnsmadeeasy.NewDefaultConfig()\n\tproviderConfig.APIKey = config.ApiKey\n\tproviderConfig.APISecret = config.ApiSecret\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := dnsmadeeasy.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/duckdns/duckdns.go",
    "content": "package namedotcom\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/duckdns\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tToken                 string `json:\"token\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := duckdns.NewDefaultConfig()\n\tproviderConfig.Token = config.Token\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\n\tprovider, err := duckdns.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/dynu/dynu.go",
    "content": "package dynu\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/dynu\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiKey                string `json:\"apiKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := dynu.NewDefaultConfig()\n\tproviderConfig.APIKey = config.ApiKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := dynu.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/dynv6/dynv6.go",
    "content": "package dynv6\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dynv6/internal\"\n)\n\ntype ChallengerConfig struct {\n\tHttpToken             string `json:\"httpToken\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := internal.NewDefaultConfig()\n\tproviderConfig.HTTPToken = config.HttpToken\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := internal.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/dynv6/internal/lego.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/samber/lo\"\n\n\tdynv6sdk \"github.com/certimate-go/certimate/pkg/sdk3rd/dynv6\"\n)\n\nconst (\n\tenvNamespace = \"DYNV6_\"\n\n\tEnvHTTPToken = envNamespace + \"HTTP_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\ntype Config struct {\n\tHTTPToken string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        time.Duration\n}\n\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *dynv6sdk.Client\n\n\tzoneIDs     map[string]int64 // Key: ZoneName; Value: ZoneID\n\tzoneIDsMu   sync.Mutex\n\trecordIDs   map[string]int64 // Key: ChallengeToken; Value: RecordID\n\trecordIDsMu sync.Mutex\n}\n\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t}\n}\n\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvHTTPToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dynv6: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.HTTPToken = values[EnvHTTPToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"dynv6: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := dynv6sdk.NewClient(config.HTTPToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dnsexit: %w\", err)\n\t} else {\n\t\tclient.SetTimeout(config.HTTPTimeout)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:      config,\n\t\tclient:      client,\n\t\tzoneIDs:     make(map[string]int64),\n\t\tzoneIDsMu:   sync.Mutex{},\n\t\trecordIDs:   make(map[string]int64),\n\t\trecordIDsMu: sync.Mutex{},\n\t}, nil\n}\n\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dynv6: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dynv6: %w\", err)\n\t}\n\n\tzone, err := d.findZone(dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dynv6: error when list zones: %w\", err)\n\t}\n\n\t// REF: https://dynv6.github.io/api-spec/#tag/records/operation/addRecord\n\tresponse, err := d.client.AddRecord(zone.ID, &dynv6sdk.AddRecordRequest{\n\t\tType: lo.ToPtr(\"TXT\"),\n\t\tName: lo.ToPtr(subDomain),\n\t\tData: lo.ToPtr(info.Value),\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dynv6: error when create record: %w\", err)\n\t}\n\n\td.zoneIDsMu.Lock()\n\td.zoneIDs[zone.Name] = zone.ID\n\td.zoneIDsMu.Unlock()\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = response.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dynv6: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\td.zoneIDsMu.Lock()\n\tzoneId, ok := d.zoneIDs[dns01.UnFqdn(authZone)]\n\td.zoneIDsMu.Unlock()\n\tif !ok {\n\t\treturn fmt.Errorf(\"dynv6: unknown zone ID for '%s'\", dns01.UnFqdn(authZone))\n\t}\n\n\td.recordIDsMu.Lock()\n\trecordId, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\tif !ok {\n\t\treturn fmt.Errorf(\"dynv6: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\tif _, err := d.client.DeleteRecord(zoneId, recordId); err != nil {\n\t\treturn fmt.Errorf(\"dynv6: error when delete record: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) findZone(zoneName string) (*dynv6sdk.ZoneRecord, error) {\n\t// REF: https://dynv6.github.io/api-spec/#tag/zones/operation/findZones\n\tzones, err := d.client.ListZones()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, zone := range *zones {\n\t\tif zone.Name == zoneName {\n\t\t\treturn zone, nil\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"could not find zone: '%s'\", zoneName)\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/gandinet/gandinet.go",
    "content": "package gandinet\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/gandiv5\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tPersonalAccessToken   string `json:\"personalAccessToken\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := gandiv5.NewDefaultConfig()\n\tproviderConfig.PersonalAccessToken = config.PersonalAccessToken\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := gandiv5.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/gcore/gcore.go",
    "content": "package gcore\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/gcore\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiToken              string `json:\"apiToken\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := gcore.NewDefaultConfig()\n\tproviderConfig.APIToken = config.ApiToken\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := gcore.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/gname/gname.go",
    "content": "package gname\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/gname/internal\"\n)\n\ntype ChallengerConfig struct {\n\tAppId                 string `json:\"appId\"`\n\tAppKey                string `json:\"appKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := internal.NewDefaultConfig()\n\tproviderConfig.AppID = config.AppId\n\tproviderConfig.AppKey = config.AppKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := internal.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/gname/internal/lego.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/samber/lo\"\n\n\tgnamesdk \"github.com/certimate-go/certimate/pkg/sdk3rd/gname\"\n)\n\nconst (\n\tenvNamespace = \"GNAME_\"\n\n\tEnvAppID  = envNamespace + \"APP_ID\"\n\tEnvAppKey = envNamespace + \"APP_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\ntype Config struct {\n\tAppID  string\n\tAppKey string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        time.Duration\n}\n\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *gnamesdk.Client\n\n\trecordIDs   map[string]int64 // Key: ChallengeToken; Value: RecordID\n\trecordIDsMu sync.Mutex\n}\n\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t}\n}\n\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAppID, EnvAppKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"gname: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AppID = values[EnvAppID]\n\tconfig.AppKey = values[EnvAppKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"gname: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := gnamesdk.NewClient(config.AppID, config.AppKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"gname: %w\", err)\n\t} else {\n\t\tclient.SetTimeout(config.HTTPTimeout)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:      config,\n\t\tclient:      client,\n\t\trecordIDs:   make(map[string]int64),\n\t\trecordIDsMu: sync.Mutex{},\n\t}, nil\n}\n\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gname: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gname: %w\", err)\n\t}\n\n\t// REF: https://www.gname.vip/domain/api/dns/add\n\trequest := &gnamesdk.AddDomainResolutionRequest{\n\t\tZoneName:    lo.ToPtr(dns01.UnFqdn(authZone)),\n\t\tRecordType:  lo.ToPtr(\"TXT\"),\n\t\tRecordName:  lo.ToPtr(subDomain),\n\t\tRecordValue: lo.ToPtr(info.Value),\n\t\tTTL:         lo.ToPtr(int32(d.config.TTL)),\n\t}\n\tresponse, err := d.client.AddDomainResolution(request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gname: error when create record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token], _ = response.Data.Int64()\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gname: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\tif !ok {\n\t\treturn fmt.Errorf(\"gname: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\t// REF: https://www.gname.vip/domain/api/dns/del\n\trequest := &gnamesdk.DeleteDomainResolutionRequest{\n\t\tZoneName: lo.ToPtr(dns01.UnFqdn(authZone)),\n\t\tRecordID: lo.ToPtr(recordID),\n\t}\n\t_, err = d.client.DeleteDomainResolution(request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gname: error when delete record: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/godaddy/godaddy.go",
    "content": "package godaddy\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/godaddy\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiKey                string `json:\"apiKey\"`\n\tApiSecret             string `json:\"apiSecret\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := godaddy.NewDefaultConfig()\n\tproviderConfig.APIKey = config.ApiKey\n\tproviderConfig.APISecret = config.ApiSecret\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := godaddy.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/hetzner/hetzner.go",
    "content": "package hetzner\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/hetzner\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiToken              string `json:\"apiToken\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := hetzner.NewDefaultConfig()\n\tproviderConfig.APIToken = config.ApiToken\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := hetzner.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/hostingde/hostingde.go",
    "content": "package hostingde\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/hostingde\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiKey                string `json:\"apiKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := hostingde.NewDefaultConfig()\n\tproviderConfig.APIKey = config.ApiKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := hostingde.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/hostinger/hostinger.go",
    "content": "package hostinger\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/hostinger\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiToken              string `json:\"apiToken\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := hostinger.NewDefaultConfig()\n\tproviderConfig.APIToken = config.ApiToken\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := hostinger.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/huaweicloud/huaweicloud.go",
    "content": "package huaweicloud\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n\thwc \"github.com/go-acme/lego/v4/providers/dns/huaweicloud\"\n)\n\ntype ChallengerConfig struct {\n\tAccessKeyId           string `json:\"accessKeyId\"`\n\tSecretAccessKey       string `json:\"secretAccessKey\"`\n\tRegion                string `json:\"region\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tregion := config.Region\n\tif region == \"\" {\n\t\t// 华为云的 SDK 要求必须传一个区域，实际上 DNS 服务用不到，但不传会报错\n\t\tregion = \"cn-north-1\"\n\t}\n\n\tproviderConfig := hwc.NewDefaultConfig()\n\tproviderConfig.AccessKeyID = config.AccessKeyId\n\tproviderConfig.SecretAccessKey = config.SecretAccessKey\n\tproviderConfig.Region = region\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = int32(config.DnsTTL)\n\t}\n\n\tprovider, err := hwc.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/infomaniak/infomaniak.go",
    "content": "package infomaniak\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/infomaniak\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tAccessToken           string `json:\"accessToken\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := infomaniak.NewDefaultConfig()\n\tproviderConfig.AccessToken = config.AccessToken\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := infomaniak.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/ionos/ionos.go",
    "content": "package ionos\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/ionos\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiKeyPublicPrefix    string `json:\"apiKeyPublicPrefix\"`\n\tApiKeySecret          string `json:\"apiKeySecret\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := ionos.NewDefaultConfig()\n\tproviderConfig.APIKey = fmt.Sprintf(\"%s.%s\", config.ApiKeyPublicPrefix, config.ApiKeySecret)\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := ionos.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/jdcloud/jdcloud.go",
    "content": "package jdcloud\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/jdcloud\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tAccessKeyId           string `json:\"accessKeyId\"`\n\tAccessKeySecret       string `json:\"accessKeySecret\"`\n\tRegionId              string `json:\"regionId\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tregionId := config.RegionId\n\tif regionId == \"\" {\n\t\t// 京东云的 SDK 要求必须传一个区域，实际上 DNS 服务用不到，但不传会报错\n\t\tregionId = \"cn-north-1\"\n\t}\n\n\tproviderConfig := jdcloud.NewDefaultConfig()\n\tproviderConfig.AccessKeyID = config.AccessKeyId\n\tproviderConfig.AccessKeySecret = config.AccessKeySecret\n\tproviderConfig.RegionID = regionId\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := jdcloud.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/linode/linode.go",
    "content": "package linode\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/linode\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tAccessToken           string `json:\"accessToken\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := linode.NewDefaultConfig()\n\tproviderConfig.Token = config.AccessToken\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := linode.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/namecheap/namecheap.go",
    "content": "package namedotcom\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/namecheap\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tUsername              string `json:\"username\"`\n\tApiKey                string `json:\"apiKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := namecheap.NewDefaultConfig()\n\tproviderConfig.APIUser = config.Username\n\tproviderConfig.APIKey = config.ApiKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := namecheap.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/namedotcom/namedotcom.go",
    "content": "package namedotcom\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/namedotcom\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tUsername              string `json:\"username\"`\n\tApiToken              string `json:\"apiToken\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := namedotcom.NewDefaultConfig()\n\tproviderConfig.Username = config.Username\n\tproviderConfig.APIToken = config.ApiToken\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := namedotcom.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/namesilo/namesilo.go",
    "content": "package namesilo\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/namesilo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiKey                string `json:\"apiKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := namesilo.NewDefaultConfig()\n\tproviderConfig.APIKey = config.ApiKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := namesilo.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/netcup/netcup.go",
    "content": "package netcup\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/netcup\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tCustomerNumber        string `json:\"customerNumber\"`\n\tApiKey                string `json:\"apiKey\"`\n\tApiPassword           string `json:\"apiPassword\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := netcup.NewDefaultConfig()\n\tproviderConfig.Customer = config.CustomerNumber\n\tproviderConfig.Key = config.ApiKey\n\tproviderConfig.Password = config.ApiPassword\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := netcup.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/netlify/netlify.go",
    "content": "package netcup\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/netlify\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiToken              string `json:\"apiToken\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := netlify.NewDefaultConfig()\n\tproviderConfig.Token = config.ApiToken\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := netlify.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/ns1/ns1.go",
    "content": "package ns1\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/ns1\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiKey                string `json:\"apiKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := ns1.NewDefaultConfig()\n\tproviderConfig.APIKey = config.ApiKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := ns1.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/ovhcloud/consts.go",
    "content": "package ovhcloud\n\nconst (\n\tAUTH_METHOD_APPLICATION = \"application\"\n\tAUTH_METHOD_OAUTH2      = \"oauth2\"\n)\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/ovhcloud/ovhcloud.go",
    "content": "package ovhcloud\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/ovh\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tEndpoint              string `json:\"endpoint\"`\n\tAuthMethod            string `json:\"authMethod\"`\n\tApplicationKey        string `json:\"applicationKey,omitempty\"`\n\tApplicationSecret     string `json:\"applicationSecret,omitempty\"`\n\tConsumerKey           string `json:\"consumerKey,omitempty\"`\n\tClientId              string `json:\"clientId,omitempty\"`\n\tClientSecret          string `json:\"clientSecret,omitempty\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := ovh.NewDefaultConfig()\n\tproviderConfig.APIEndpoint = config.Endpoint\n\tswitch config.AuthMethod {\n\tcase AUTH_METHOD_APPLICATION:\n\t\tproviderConfig.ApplicationKey = config.ApplicationKey\n\t\tproviderConfig.ApplicationSecret = config.ApplicationSecret\n\t\tproviderConfig.ConsumerKey = config.ConsumerKey\n\tcase AUTH_METHOD_OAUTH2:\n\t\tproviderConfig.OAuth2Config = &ovh.OAuth2Config{\n\t\t\tClientID:     config.ClientId,\n\t\t\tClientSecret: config.ClientSecret,\n\t\t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported auth method '%s'\", config.AuthMethod)\n\t}\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := ovh.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/porkbun/porkbun.go",
    "content": "package porkbun\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/porkbun\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiKey                string `json:\"apiKey\"`\n\tSecretApiKey          string `json:\"secretApiKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := porkbun.NewDefaultConfig()\n\tproviderConfig.APIKey = config.ApiKey\n\tproviderConfig.SecretAPIKey = config.SecretApiKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := porkbun.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/powerdns/powerdns.go",
    "content": "package powerdns\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/pdns\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n\txhttp \"github.com/certimate-go/certimate/pkg/utils/http\"\n)\n\ntype ChallengerConfig struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiKey                   string `json:\"apiKey\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n\tDnsPropagationTimeout    int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                   int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tserverUrl, _ := url.Parse(config.ServerUrl)\n\tproviderConfig := pdns.NewDefaultConfig()\n\tproviderConfig.Host = serverUrl\n\tproviderConfig.APIKey = config.ApiKey\n\tif config.AllowInsecureConnections {\n\t\ttransport := xhttp.NewDefaultTransport()\n\t\ttransport.DisableKeepAlives = true\n\t\ttransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}\n\t\tproviderConfig.HTTPClient.Transport = transport\n\t}\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := pdns.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/qingcloud/internal/lego.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/samber/lo\"\n\n\tqingcloudsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/qingcloud/dns\"\n)\n\nconst (\n\tenvNamespace = \"QINGCLOUD_\"\n\n\tEnvAccessKey    = envNamespace + \"ACCESS_KEY\"\n\tEnvAccessSecret = envNamespace + \"ACCESS_SECRET\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\ntype Config struct {\n\tAccessKey    string\n\tAccessSecret string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        time.Duration\n}\n\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *qingcloudsdk.Client\n\n\trecordIDs   map[string]*int64 // Key: ChallengeToken; Value: RecordID\n\trecordIDsMu sync.Mutex\n}\n\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t}\n}\n\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAccessKey, EnvAccessSecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"qingcloud: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AccessKey = values[EnvAccessKey]\n\tconfig.AccessSecret = values[EnvAccessSecret]\n\n\treturn NewDNSProviderConfig(config)\n}\n\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"qingcloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := qingcloudsdk.NewClient(config.AccessKey, config.AccessSecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"qingcloud: %w\", err)\n\t} else {\n\t\tclient.SetTimeout(config.HTTPTimeout)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:      config,\n\t\tclient:      client,\n\t\trecordIDs:   make(map[string]*int64),\n\t\trecordIDsMu: sync.Mutex{},\n\t}, nil\n}\n\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"qingcloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// REF: https://docsv4.qingcloud.com/user_guide/development_docs/api/api_list/dns/record/#_createrecord\n\trequest := &qingcloudsdk.CreateRecordRequest{\n\t\tZoneName:   lo.ToPtr(authZone),\n\t\tDomainName: lo.ToPtr(info.EffectiveFQDN),\n\t\tViewId:     lo.ToPtr(int32(0)),\n\t\tType:       lo.ToPtr(\"TXT\"),\n\t\tTtl:        lo.ToPtr(int32(d.config.TTL)),\n\t\tRecords: []*qingcloudsdk.CreateRecordRequestRecord{\n\t\t\t{\n\t\t\t\tValues: []*qingcloudsdk.CreateRecordRequestRecordValue{\n\t\t\t\t\t{\n\t\t\t\t\t\tValue:  lo.ToPtr(info.Value),\n\t\t\t\t\t\tStatus: lo.ToPtr(int32(1)),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWeight: lo.ToPtr(int32(0)),\n\t\t\t},\n\t\t},\n\t\tMode:      lo.ToPtr(int32(1)),\n\t\tAutoMerge: lo.ToPtr(int32(1)),\n\t}\n\tresponse, err := d.client.CreateRecord(request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"qingcloud: error when create record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = response.DomainRecordId\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\tif !ok {\n\t\treturn fmt.Errorf(\"qingcloud: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\t// REF: https://docsv4.qingcloud.com/user_guide/development_docs/api/api_list/dns/record/#_deleterecord\n\tif _, err := d.client.DeleteRecord([]*int64{recordID}); err != nil {\n\t\treturn fmt.Errorf(\"qingcloud: error when delete record: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/qingcloud/qingcloud.go",
    "content": "package qingcloud\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/qingcloud/internal\"\n)\n\ntype ChallengerConfig struct {\n\tAccessKeyId           string `json:\"accessKeyId\"`\n\tSecretAccessKey       string `json:\"apiPassword\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := internal.NewDefaultConfig()\n\tproviderConfig.AccessKey = config.AccessKeyId\n\tproviderConfig.AccessSecret = config.SecretAccessKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := internal.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/rainyun/rainyun.go",
    "content": "package rainyun\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/rainyun\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiKey                string `json:\"apiKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := rainyun.NewDefaultConfig()\n\tproviderConfig.APIKey = config.ApiKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := rainyun.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/rfc2136/rfc2136.go",
    "content": "package rfc2136\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/rfc2136\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tHost                  string `json:\"host\"`\n\tPort                  int32  `json:\"port\"`\n\tTsigAlgorithm         string `json:\"tsigAlgorithm,omitempty\"`\n\tTsigKey               string `json:\"tsigKey,omitempty\"`\n\tTsigSecret            string `json:\"tsigSecret,omitempty\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tif config.Port == 0 {\n\t\tconfig.Port = 53\n\t}\n\n\tif config.TsigAlgorithm == \"\" {\n\t\tconfig.TsigAlgorithm = \"hmac-sha1.\"\n\t}\n\n\tproviderConfig := rfc2136.NewDefaultConfig()\n\tproviderConfig.Nameserver = net.JoinHostPort(config.Host, strconv.Itoa(int(config.Port)))\n\tproviderConfig.TSIGAlgorithm = config.TsigAlgorithm\n\tproviderConfig.TSIGKey = config.TsigKey\n\tproviderConfig.TSIGSecret = config.TsigSecret\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := rfc2136.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/spaceship/spaceship.go",
    "content": "package spaceship\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/spaceship\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiKey                string `json:\"apiKey\"`\n\tApiSecret             string `json:\"apiSecret\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := spaceship.NewDefaultConfig()\n\tproviderConfig.APIKey = config.ApiKey\n\tproviderConfig.APISecret = config.ApiSecret\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := spaceship.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/technitiumdns/technitiumdns.go",
    "content": "package technitiumdns\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/technitium\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n\txhttp \"github.com/certimate-go/certimate/pkg/utils/http\"\n)\n\ntype ChallengerConfig struct {\n\tServerUrl                string `json:\"serverUrl\"`\n\tApiToken                 string `json:\"apiToken\"`\n\tAllowInsecureConnections bool   `json:\"allowInsecureConnections,omitempty\"`\n\tDnsPropagationTimeout    int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                   int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := technitium.NewDefaultConfig()\n\tproviderConfig.BaseURL = config.ServerUrl\n\tproviderConfig.APIToken = config.ApiToken\n\tif config.AllowInsecureConnections {\n\t\ttransport := xhttp.NewDefaultTransport()\n\t\ttransport.DisableKeepAlives = true\n\t\ttransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}\n\t\tproviderConfig.HTTPClient.Transport = transport\n\t}\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := technitium.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/tencentcloud/tencentcloud.go",
    "content": "package tencentcloud\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/tencentcloud\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tSecretId              string `json:\"secretId\"`\n\tSecretKey             string `json:\"secretKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := tencentcloud.NewDefaultConfig()\n\tproviderConfig.SecretID = config.SecretId\n\tproviderConfig.SecretKey = config.SecretKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := tencentcloud.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/tencentcloud-eo/tencentcloud_eo.go",
    "content": "package tencentcloudeo\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/edgeone\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tSecretId              string `json:\"secretId\"`\n\tSecretKey             string `json:\"secretKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := edgeone.NewDefaultConfig()\n\tproviderConfig.SecretID = config.SecretId\n\tproviderConfig.SecretKey = config.SecretKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := edgeone.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/todaynic/todaynic.go",
    "content": "package todaynic\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/todaynic\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tUserId                string `json:\"userId\"`\n\tApiKey                string `json:\"apiKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := todaynic.NewDefaultConfig()\n\tproviderConfig.AuthUserID = config.UserId\n\tproviderConfig.APIKey = config.ApiKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := todaynic.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/ucloud/internal/lego.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n\n\t\"github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/udnr\"\n)\n\nconst (\n\tenvNamespace = \"UCLOUDUDNR_\"\n\n\tEnvPublicKey  = envNamespace + \"PUBLIC_KEY\"\n\tEnvPrivateKey = envNamespace + \"PRIVATE_KEY\"\n\tEnvProjectId  = envNamespace + \"PROJECT_ID\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\ntype Config struct {\n\tPrivateKey string\n\tPublicKey  string\n\tProjectId  string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        time.Duration\n}\n\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *udnr.UDNRClient\n}\n\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t}\n}\n\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvPrivateKey, EnvPublicKey, EnvProjectId)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ucloud: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.PrivateKey = values[EnvPrivateKey]\n\tconfig.PublicKey = values[EnvPublicKey]\n\tconfig.ProjectId = values[EnvProjectId]\n\n\treturn NewDNSProviderConfig(config)\n}\n\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"ucloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tcfg := ucloud.NewConfig()\n\tcfg.Timeout = config.HTTPTimeout\n\tcfg.ProjectId = config.ProjectId\n\tcredential := auth.NewCredential()\n\tcredential.PrivateKey = config.PrivateKey\n\tcredential.PublicKey = config.PublicKey\n\tclient := udnr.NewClient(&cfg, &credential)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ucloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// REF: https://docs.ucloud.cn/api/udnr-api/udnr_domain_dns_add\n\trequest := d.client.NewAddDomainDNSRequest()\n\trequest.Dn = ucloud.String(dns01.UnFqdn(authZone))\n\trequest.DnsType = ucloud.String(\"TXT\")\n\trequest.RecordName = ucloud.String(dns01.UnFqdn(info.EffectiveFQDN))\n\trequest.Content = ucloud.String(info.Value)\n\trequest.TTL = ucloud.String(fmt.Sprintf(\"%d\", d.config.TTL))\n\tif _, err := d.client.AddDomainDNS(request); err != nil {\n\t\treturn fmt.Errorf(\"ucloud: error when create record: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ucloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// REF: https://docs.ucloud.cn/api/udnr-api/udnr_domain_dns_query\n\trequest := d.client.NewQueryDomainDNSRequest()\n\trequest.Dn = ucloud.String(dns01.UnFqdn(authZone))\n\tresponse, err := d.client.QueryDomainDNS(request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ucloud: error when list records: %w\", err)\n\t}\n\n\t// REF: https://docs.ucloud.cn/api/udnr-api/udnr_delete_dns_record\n\tfor _, record := range response.Data {\n\t\tif record.DnsType == \"TXT\" && record.RecordName == dns01.UnFqdn(info.EffectiveFQDN) && record.Content == info.Value {\n\t\t\tdelreq := d.client.NewDeleteDomainDNSRequest()\n\t\t\tdelreq.Dn = ucloud.String(dns01.UnFqdn(authZone))\n\t\t\tdelreq.DnsType = ucloud.String(record.DnsType)\n\t\t\tdelreq.RecordName = ucloud.String(record.RecordName)\n\t\t\tdelreq.Content = ucloud.String(record.Content)\n\t\t\t_, err := d.client.DeleteDomainDNS(delreq)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"ucloud: error when delete record: %w\", err)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/ucloud/ucloud.go",
    "content": "package ucloud\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/ucloud/internal\"\n)\n\ntype ChallengerConfig struct {\n\tPrivateKey            string `json:\"privateKey\"`\n\tPublicKey             string `json:\"publicKey\"`\n\tProjectId             string `json:\"projectId,omitempty\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"config is nil\")\n\t}\n\n\tproviderConfig := internal.NewDefaultConfig()\n\tproviderConfig.PrivateKey = config.PrivateKey\n\tproviderConfig.PublicKey = config.PublicKey\n\tproviderConfig.ProjectId = config.ProjectId\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\n\tprovider, err := internal.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/vercel/vercel.go",
    "content": "package vercel\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/vercel\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiAccessToken        string `json:\"apiAccessToken\"`\n\tTeamId                string `json:\"teamId,omitempty\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := vercel.NewDefaultConfig()\n\tproviderConfig.AuthToken = config.ApiAccessToken\n\tproviderConfig.TeamID = config.TeamId\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := vercel.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/volcengine/volcengine.go",
    "content": "package volcengine\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/volcengine\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tAccessKeyId           string `json:\"accessKeyId\"`\n\tSecretAccessKey       string `json:\"secretAccessKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := volcengine.NewDefaultConfig()\n\tproviderConfig.AccessKey = config.AccessKeyId\n\tproviderConfig.SecretKey = config.SecretAccessKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := volcengine.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/vultr/vultr.go",
    "content": "package vultr\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/vultr\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tApiKey                string `json:\"apiKey\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := vultr.NewDefaultConfig()\n\tproviderConfig.APIKey = config.ApiKey\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := vultr.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/westcn/westcn.go",
    "content": "package westcn\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/westcn\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\tUsername              string `json:\"username\"`\n\tApiPassword           string `json:\"apiPassword\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := westcn.NewDefaultConfig()\n\tproviderConfig.Username = config.Username\n\tproviderConfig.Password = config.ApiPassword\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := westcn.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/xinnet/internal/lego.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/samber/lo\"\n\n\txinnetsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/xinnet\"\n)\n\nconst (\n\tenvNamespace = \"XINNET_\"\n\n\tEnvAgentId   = envNamespace + \"AGENT_ID\"\n\tEnvAppSecret = envNamespace + \"APP_SECRET\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\ntype Config struct {\n\tAgentID   string\n\tAppSecret string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        time.Duration\n}\n\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *xinnetsdk.Client\n\n\trecordIDs   map[string]*int64 // Key: ChallengeToken; Value: RecordID\n\trecordIDsMu sync.Mutex\n}\n\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t}\n}\n\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAgentId, EnvAppSecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"xinnet: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AgentID = values[EnvAgentId]\n\tconfig.AppSecret = values[EnvAppSecret]\n\n\treturn NewDNSProviderConfig(config)\n}\n\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"xinnet: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := xinnetsdk.NewClient(config.AgentID, config.AppSecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"xinnet: %w\", err)\n\t} else {\n\t\tclient.SetTimeout(config.HTTPTimeout)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:      config,\n\t\tclient:      client,\n\t\trecordIDs:   make(map[string]*int64),\n\t\trecordIDsMu: sync.Mutex{},\n\t}, nil\n}\n\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"xinnet: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// REF: https://apidoc.xin.cn/doc-7283900\n\trequest := &xinnetsdk.DnsCreateRequest{\n\t\tDomainName: lo.ToPtr(dns01.UnFqdn(authZone)),\n\t\tRecordName: lo.ToPtr(dns01.UnFqdn(info.EffectiveFQDN)),\n\t\tType:       lo.ToPtr(\"TXT\"),\n\t\tValue:      lo.ToPtr(info.Value),\n\t\tLine:       lo.ToPtr(\"默认\"),\n\t\tTtl:        lo.ToPtr(int32(d.config.TTL)),\n\t}\n\tresponse, err := d.client.DnsCreate(request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"xinnet: error when create record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = response.Data\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"xinnet: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\tif !ok {\n\t\treturn fmt.Errorf(\"xinnet: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\t// REF: https://apidoc.xin.cn/doc-7283901\n\trequest := &xinnetsdk.DnsDeleteRequest{\n\t\tDomainName: lo.ToPtr(dns01.UnFqdn(authZone)),\n\t\tRecordId:   recordID,\n\t}\n\tif _, err := d.client.DnsDelete(request); err != nil {\n\t\treturn fmt.Errorf(\"xinnet: error when delete record: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/dns01/xinnet/xinnet.go",
    "content": "package xinnet\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/xinnet/internal\"\n)\n\ntype ChallengerConfig struct {\n\tAgentId               string `json:\"agentId\"`\n\tApiPassword           string `json:\"apiPassword\"`\n\tDnsPropagationTimeout int    `json:\"dnsPropagationTimeout,omitempty\"`\n\tDnsTTL                int    `json:\"dnsTTL,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tproviderConfig := internal.NewDefaultConfig()\n\tproviderConfig.AgentID = config.AgentId\n\tproviderConfig.AppSecret = config.ApiPassword\n\tif config.DnsPropagationTimeout != 0 {\n\t\tproviderConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second\n\t}\n\tif config.DnsTTL != 0 {\n\t\tproviderConfig.TTL = config.DnsTTL\n\t}\n\n\tprovider, err := internal.NewDNSProviderConfig(providerConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/http01/local/local.go",
    "content": "package local\n\nimport (\n\t\"errors\"\n\n\t\"github.com/go-acme/lego/v4/providers/http/webroot\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\t// 网站根目录路径。\n\tWebRootPath string `json:\"webRootPath\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tprovider, err := webroot.NewHTTPProvider(config.WebRootPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn provider, nil\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/http01/s3/s3.go",
    "content": "package s3\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/go-acme/lego/v4/challenge/http01\"\n\n\t\"github.com/certimate-go/certimate/internal/tools/s3\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n)\n\ntype ChallengerConfig struct {\n\t// S3 Endpoint。\n\tEndpoint string `json:\"endpoint\"`\n\t// S3 AccessKey。\n\tAccessKey string `json:\"accessKey\"`\n\t// S3 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// S3 签名版本。\n\t// 可取值 \"v2\"、\"v4\"。\n\t// 零值时默认值 \"v4\"。\n\tSignatureVersion string `json:\"signatureVersion,omitempty\"`\n\t// 是否使用路径风格。\n\tUsePathStyle bool `json:\"usePathStyle,omitempty\"`\n\t// 存储区域。\n\tRegion string `json:\"region\"`\n\t// 存储桶名。\n\tBucket string `json:\"bucket\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tclient, err := createS3Client(*config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"s3: failed to create S3 client: %w\", err)\n\t}\n\n\tprovider := &provider{client: client, bucket: config.Bucket}\n\treturn provider, nil\n}\n\ntype provider struct {\n\tclient *s3.Client\n\tbucket string\n}\n\nfunc (p *provider) Present(domain, token, keyAuth string) error {\n\tobjectKey := strings.Trim(http01.ChallengePath(token), \"/\")\n\tif err := p.client.PutObjectString(context.Background(), p.bucket, objectKey, keyAuth); err != nil {\n\t\treturn fmt.Errorf(\"s3: failed to upload token to s3: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (p *provider) CleanUp(domain, token, keyAuth string) error {\n\tobjectKey := strings.Trim(http01.ChallengePath(token), \"/\")\n\tif err := p.client.RemoveObject(context.Background(), p.bucket, objectKey); err != nil {\n\t\treturn fmt.Errorf(\"s3: could not remove file in s3 bucket after HTTP challenge: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createS3Client(config ChallengerConfig) (*s3.Client, error) {\n\tclientCfg := s3.NewDefaultConfig()\n\tclientCfg.Endpoint = config.Endpoint\n\tclientCfg.AccessKey = config.AccessKey\n\tclientCfg.SecretKey = config.SecretKey\n\tclientCfg.SignatureVersion = config.SignatureVersion\n\tclientCfg.UsePathStyle = config.UsePathStyle\n\tclientCfg.Region = config.Region\n\tclientCfg.SkipTlsVerify = config.AllowInsecureConnections\n\n\tclient, err := s3.NewClient(clientCfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, err\n}\n"
  },
  {
    "path": "pkg/core/certifier/challengers/http01/ssh/ssh.go",
    "content": "package ssh\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\n\t\"github.com/go-acme/lego/v4/challenge/http01\"\n\n\t\"github.com/certimate-go/certimate/internal/tools/ssh\"\n\t\"github.com/certimate-go/certimate/pkg/core/certifier\"\n\txssh \"github.com/certimate-go/certimate/pkg/utils/ssh\"\n)\n\ntype ServerConfig struct {\n\t// SSH 主机。\n\t// 零值时默认值 \"localhost\"。\n\tSshHost string `json:\"sshHost,omitempty\"`\n\t// SSH 端口。\n\t// 零值时默认值 22。\n\tSshPort int32 `json:\"sshPort,omitempty\"`\n\t// SSH 认证方式。\n\t// 可取值 \"none\"、\"password\"、\"key\"。\n\t// 零值时根据有无密码或私钥字段决定。\n\tSshAuthMethod string `json:\"sshAuthMethod,omitempty\"`\n\t// SSH 登录用户名。\n\t// 零值时默认值 \"root\"。\n\tSshUsername string `json:\"sshUsername,omitempty\"`\n\t// SSH 登录密码。\n\tSshPassword string `json:\"sshPassword,omitempty\"`\n\t// SSH 登录私钥。\n\tSshKey string `json:\"sshKey,omitempty\"`\n\t// SSH 登录私钥口令。\n\tSshKeyPassphrase string `json:\"sshKeyPassphrase,omitempty\"`\n}\n\ntype ChallengerConfig struct {\n\tServerConfig\n\n\t// 跳板机配置数组。\n\tJumpServers []ServerConfig `json:\"jumpServers,omitempty\"`\n\t// 是否回退使用 SCP。\n\tUseSCP bool `json:\"useSCP,omitempty\"`\n\t// 网站根目录路径。\n\tWebRootPath string `json:\"webRootPath\"`\n}\n\nfunc NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the acme challenge provider is nil\")\n\t}\n\n\tprovider := &provider{config: config}\n\treturn provider, nil\n}\n\ntype provider struct {\n\tconfig *ChallengerConfig\n}\n\nfunc (p *provider) Present(domain, token, keyAuth string) error {\n\tclient, err := createSshClient(*p.config)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ssh: failed to create SSH client: %w\", err)\n\t}\n\n\tdefer client.Close()\n\n\tchallengeFilePath := filepath.Join(p.config.WebRootPath, http01.ChallengePath(token))\n\tif err := xssh.WriteRemoteString(client.GetClient(), challengeFilePath, keyAuth, p.config.UseSCP); err != nil {\n\t\treturn fmt.Errorf(\"failed to write file in webroot for HTTP challenge: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (p *provider) CleanUp(domain, token, keyAuth string) error {\n\tclient, err := createSshClient(*p.config)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ssh: failed to create SSH client: %w\", err)\n\t}\n\n\tdefer client.Close()\n\n\t// 删除质询文件\n\tchallengeFilePath := filepath.Join(p.config.WebRootPath, http01.ChallengePath(token))\n\txssh.RemoveRemote(client.GetClient(), challengeFilePath, p.config.UseSCP)\n\n\treturn nil\n}\n\nfunc createSshClient(config ChallengerConfig) (*ssh.Client, error) {\n\tclientCfg := ssh.NewDefaultConfig()\n\tclientCfg.Host = config.SshHost\n\tclientCfg.Port = int(config.SshPort)\n\tclientCfg.AuthMethod = ssh.AuthMethodType(config.SshAuthMethod)\n\tclientCfg.Username = config.SshUsername\n\tclientCfg.Password = config.SshPassword\n\tclientCfg.Key = config.SshKey\n\tclientCfg.KeyPassphrase = config.SshKeyPassphrase\n\tfor _, jumpServer := range config.JumpServers {\n\t\tjumpServerCfg := ssh.NewServerConfig()\n\t\tjumpServerCfg.Host = jumpServer.SshHost\n\t\tjumpServerCfg.Port = int(jumpServer.SshPort)\n\t\tjumpServerCfg.AuthMethod = ssh.AuthMethodType(jumpServer.SshAuthMethod)\n\t\tjumpServerCfg.Username = jumpServer.SshUsername\n\t\tjumpServerCfg.Password = jumpServer.SshPassword\n\t\tjumpServerCfg.Key = jumpServer.SshKey\n\t\tjumpServerCfg.KeyPassphrase = jumpServer.SshKeyPassphrase\n\t\tclientCfg.JumpServers = append(clientCfg.JumpServers, *jumpServerCfg)\n\t}\n\n\tclient, err := ssh.NewClient(clientCfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/errors.go",
    "content": "package certmgr\n\nimport (\n\t\"errors\"\n)\n\nvar (\n\tErrNotImplemented = errors.New(\"not implemented function\")\n\tErrUnsupported    = errors.ErrUnsupported\n)\n"
  },
  {
    "path": "pkg/core/certmgr/provider.go",
    "content": "package certmgr\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n)\n\n// 表示定义 SSL 证书管理器的抽象类型接口。\n// 云服务商通常会提供 SSL 证书管理服务，可供用户集中管理证书。\ntype Provider interface {\n\t// 设置日志记录器。\n\t//\n\t// 入参：\n\t//   - logger：日志记录器实例。\n\tSetLogger(logger *slog.Logger)\n\n\t// 上传证书。\n\t//\n\t// 入参：\n\t//   - ctx：上下文。\n\t//   - certPEM：证书 PEM 内容。\n\t//   - privkeyPEM：私钥 PEM 内容。\n\t//\n\t// 出参：\n\t//   - res：上传结果。\n\t//   - err: 错误。\n\tUpload(ctx context.Context, certPEM, privkeyPEM string) (_res *UploadResult, _err error)\n\n\t// 更新证书。\n\t//\n\t// 入参：\n\t//   - ctx：上下文。\n\t//   - certIdOrName：证书 ID 或名称，即云服务商处的证书标识符。\n\t//   - certPEM：证书 PEM 内容。\n\t//   - privkeyPEM：私钥 PEM 内容。\n\t//\n\t// 出参：\n\t//   - res：操作结果。\n\t//   - err: 错误。\n\tReplace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (_res *OperateResult, _err error)\n}\n\n// 表示 SSL 证书管理操作结果的数据结构。\ntype OperateResult struct {\n\tExtendedData map[string]any `json:\"extendedData,omitempty\"`\n}\n\n// 表示 SSL 证书管理上传结果的数据结构，包含上传后的证书 ID、名称和其他数据。\ntype UploadResult struct {\n\tOperateResult\n\tCertId       string         `json:\"certId,omitempty\"`\n\tCertName     string         `json:\"certName,omitempty\"`\n\tExtendedData map[string]any `json:\"extendedData,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/1panel/1panel.go",
    "content": "package onepanelssl\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tonepanelsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/1panel\"\n\tonepanelsdk2 \"github.com/certimate-go/certimate/pkg/sdk3rd/1panel/v2\"\n)\n\ntype CertmgrConfig struct {\n\t// 1Panel 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// 1Panel 版本。\n\tApiVersion string `json:\"apiVersion\"`\n\t// 1Panel 接口密钥。\n\tApiKey string `json:\"apiKey\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 子节点名称。\n\t// 选填。\n\tNodeName string `json:\"nodeName,omitempty\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient any\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiVersion, config.ApiKey, config.AllowInsecureConnections, config.NodeName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 避免重复上传\n\tif upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM, privkeyPEM); err != nil {\n\t\treturn nil, err\n\t} else if upok {\n\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\treturn upres, nil\n\t}\n\n\t// 生成新证书名（需符合 1Panel 命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 上传证书\n\tswitch sdkClient := c.sdkClient.(type) {\n\tcase *onepanelsdk.Client:\n\t\t{\n\t\t\twebsiteSSLUploadReq := &onepanelsdk.WebsiteSSLUploadRequest{\n\t\t\t\tType:        \"paste\",\n\t\t\t\tDescription: certName,\n\t\t\t\tCertificate: certPEM,\n\t\t\t\tPrivateKey:  privkeyPEM,\n\t\t\t}\n\t\t\twebsiteSSLUploadResp, err := sdkClient.WebsiteSSLUploadWithContext(ctx, websiteSSLUploadReq)\n\t\t\tc.logger.Debug(\"sdk request '1panel.WebsiteSSLUpload'\", slog.Any(\"request\", websiteSSLUploadReq), slog.Any(\"response\", websiteSSLUploadResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request '1panel.WebsiteSSLUpload': %w\", err)\n\t\t\t}\n\t\t}\n\n\tcase *onepanelsdk2.Client:\n\t\t{\n\t\t\twebsiteSSLUploadReq := &onepanelsdk2.WebsiteSSLUploadRequest{\n\t\t\t\tType:        \"paste\",\n\t\t\t\tDescription: certName,\n\t\t\t\tCertificate: certPEM,\n\t\t\t\tPrivateKey:  privkeyPEM,\n\t\t\t}\n\t\t\twebsiteSSLUploadResp, err := sdkClient.WebsiteSSLUploadWithContext(ctx, websiteSSLUploadReq)\n\t\t\tc.logger.Debug(\"sdk request '1panel.WebsiteSSLUpload'\", slog.Any(\"request\", websiteSSLUploadReq), slog.Any(\"response\", websiteSSLUploadResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request '1panel.WebsiteSSLUpload': %w\", err)\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\tpanic(\"unreachable\")\n\t}\n\n\t// 获取刚刚上传证书 ID\n\tif upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM, privkeyPEM); err != nil {\n\t\treturn nil, err\n\t} else if !upok {\n\t\treturn nil, fmt.Errorf(\"could not find ssl certificate, may be upload failed\")\n\t} else {\n\t\treturn upres, nil\n\t}\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\tsslId, err := strconv.ParseInt(certIdOrName, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch sdkClient := c.sdkClient.(type) {\n\tcase *onepanelsdk.Client:\n\t\t{\n\t\t\t// 获取证书详情\n\t\t\twebsiteSSLGetResp, err := sdkClient.WebsiteSSLGetWithContext(ctx, sslId)\n\t\t\tc.logger.Debug(\"sdk request '1panel.WebsiteSSLGet'\", slog.Int64(\"sslId\", sslId), slog.Any(\"response\", websiteSSLGetResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request '1panel.WebsiteSSLGet': %w\", err)\n\t\t\t}\n\n\t\t\t// 更新证书\n\t\t\twebsiteSSLUploadReq := &onepanelsdk.WebsiteSSLUploadRequest{\n\t\t\t\tSSLID:       sslId,\n\t\t\t\tType:        \"paste\",\n\t\t\t\tDescription: websiteSSLGetResp.Data.Description,\n\t\t\t\tCertificate: certPEM,\n\t\t\t\tPrivateKey:  privkeyPEM,\n\t\t\t}\n\t\t\twebsiteSSLUploadResp, err := sdkClient.WebsiteSSLUploadWithContext(ctx, websiteSSLUploadReq)\n\t\t\tc.logger.Debug(\"sdk request '1panel.WebsiteSSLUpload'\", slog.Any(\"request\", websiteSSLUploadReq), slog.Any(\"response\", websiteSSLUploadResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request '1panel.WebsiteSSLUpload': %w\", err)\n\t\t\t}\n\t\t}\n\n\tcase *onepanelsdk2.Client:\n\t\t{\n\t\t\t// 获取证书详情\n\t\t\twebsiteSSLGetResp, err := sdkClient.WebsiteSSLGetWithContext(ctx, sslId)\n\t\t\tc.logger.Debug(\"sdk request '1panel.WebsiteSSLGet'\", slog.Any(\"sslId\", sslId), slog.Any(\"response\", websiteSSLGetResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request '1panel.WebsiteSSLGet': %w\", err)\n\t\t\t}\n\n\t\t\t// 更新证书\n\t\t\twebsiteSSLUploadReq := &onepanelsdk2.WebsiteSSLUploadRequest{\n\t\t\t\tSSLID:       sslId,\n\t\t\t\tType:        \"paste\",\n\t\t\t\tDescription: websiteSSLGetResp.Data.Description,\n\t\t\t\tCertificate: certPEM,\n\t\t\t\tPrivateKey:  privkeyPEM,\n\t\t\t}\n\t\t\twebsiteSSLUploadResp, err := sdkClient.WebsiteSSLUploadWithContext(ctx, websiteSSLUploadReq)\n\t\t\tc.logger.Debug(\"sdk request '1panel.WebsiteSSLUpload'\", slog.Any(\"request\", websiteSSLUploadReq), slog.Any(\"response\", websiteSSLUploadResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request '1panel.WebsiteSSLUpload': %w\", err)\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\tpanic(\"unreachable\")\n\t}\n\n\treturn &certmgr.OperateResult{}, nil\n}\n\nfunc (c *Certmgr) tryGetResultIfCertExists(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, bool, error) {\n\tswitch sdkClient := c.sdkClient.(type) {\n\tcase *onepanelsdk.Client:\n\t\t{\n\t\t\tsearchWebsiteSSLPage := 1\n\t\t\tsearchWebsiteSSLPageSize := 100\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn nil, false, ctx.Err()\n\t\t\t\tdefault:\n\t\t\t\t}\n\n\t\t\t\twebsiteSSLSearchReq := &onepanelsdk.WebsiteSSLSearchRequest{\n\t\t\t\t\tPage:     int32(searchWebsiteSSLPage),\n\t\t\t\t\tPageSize: int32(searchWebsiteSSLPageSize),\n\t\t\t\t}\n\t\t\t\twebsiteSSLSearchResp, err := sdkClient.WebsiteSSLSearchWithContext(ctx, websiteSSLSearchReq)\n\t\t\t\tc.logger.Debug(\"sdk request '1panel.WebsiteSSLSearch'\", slog.Any(\"request\", websiteSSLSearchReq), slog.Any(\"response\", websiteSSLSearchResp))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, false, fmt.Errorf(\"failed to execute sdk request '1panel.WebsiteSSLSearch': %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif websiteSSLSearchResp.Data == nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tfor _, sslItem := range websiteSSLSearchResp.Data.Items {\n\t\t\t\t\toldCertPEM := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(sslItem.PEM, \"\\r\", \"\"), \"\\n\", \"\"))\n\t\t\t\t\toldPrivkeyPEM := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(sslItem.PrivateKey, \"\\r\", \"\"), \"\\n\", \"\"))\n\t\t\t\t\tnewCertPEM := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(certPEM, \"\\r\", \"\"), \"\\n\", \"\"))\n\t\t\t\t\tnewPrivkeyPEM := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(privkeyPEM, \"\\r\", \"\"), \"\\n\", \"\"))\n\t\t\t\t\tif oldCertPEM == newCertPEM && oldPrivkeyPEM == newPrivkeyPEM {\n\t\t\t\t\t\t// 如果已存在相同证书，直接返回\n\t\t\t\t\t\treturn &certmgr.UploadResult{\n\t\t\t\t\t\t\tCertId:   fmt.Sprintf(\"%d\", sslItem.ID),\n\t\t\t\t\t\t\tCertName: sslItem.Description,\n\t\t\t\t\t\t}, true, nil\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif len(websiteSSLSearchResp.Data.Items) < int(websiteSSLSearchResp.Data.Total) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tsearchWebsiteSSLPage++\n\t\t\t}\n\t\t}\n\n\tcase *onepanelsdk2.Client:\n\t\t{\n\t\t\tsearchWebsiteSSLPage := 1\n\t\t\tsearchWebsiteSSLPageSize := 100\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn nil, false, ctx.Err()\n\t\t\t\tdefault:\n\t\t\t\t}\n\n\t\t\t\twebsiteSSLSearchReq := &onepanelsdk2.WebsiteSSLSearchRequest{\n\t\t\t\t\tOrder:    \"null\",\n\t\t\t\t\tOrderBy:  \"expire_date\",\n\t\t\t\t\tPage:     int32(searchWebsiteSSLPage),\n\t\t\t\t\tPageSize: int32(searchWebsiteSSLPageSize),\n\t\t\t\t}\n\t\t\t\twebsiteSSLSearchResp, err := sdkClient.WebsiteSSLSearchWithContext(ctx, websiteSSLSearchReq)\n\t\t\t\tc.logger.Debug(\"sdk request '1panel.WebsiteSSLSearch'\", slog.Any(\"request\", websiteSSLSearchReq), slog.Any(\"response\", websiteSSLSearchResp))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, false, fmt.Errorf(\"failed to execute sdk request '1panel.WebsiteSSLSearch': %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif websiteSSLSearchResp.Data == nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tfor _, sslItem := range websiteSSLSearchResp.Data.Items {\n\t\t\t\t\toldCertPEM := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(sslItem.PEM, \"\\r\", \"\"), \"\\n\", \"\"))\n\t\t\t\t\toldPrivkeyPEM := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(sslItem.PrivateKey, \"\\r\", \"\"), \"\\n\", \"\"))\n\t\t\t\t\tnewCertPEM := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(certPEM, \"\\r\", \"\"), \"\\n\", \"\"))\n\t\t\t\t\tnewPrivkeyPEM := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(privkeyPEM, \"\\r\", \"\"), \"\\n\", \"\"))\n\t\t\t\t\tif oldCertPEM == newCertPEM && oldPrivkeyPEM == newPrivkeyPEM {\n\t\t\t\t\t\t// 如果已存在相同证书，直接返回\n\t\t\t\t\t\treturn &certmgr.UploadResult{\n\t\t\t\t\t\t\tCertId:   fmt.Sprintf(\"%d\", sslItem.ID),\n\t\t\t\t\t\t\tCertName: sslItem.Description,\n\t\t\t\t\t\t}, true, nil\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif len(websiteSSLSearchResp.Data.Items) < int(websiteSSLSearchResp.Data.Total) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tsearchWebsiteSSLPage++\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\tpanic(\"unreachable\")\n\t}\n\n\treturn nil, false, nil\n}\n\nconst (\n\tsdkVersionV1 = \"v1\"\n\tsdkVersionV2 = \"v2\"\n)\n\nfunc createSDKClient(serverUrl, apiVersion, apiKey string, skipTlsVerify bool, nodeName string) (any, error) {\n\tif apiVersion == sdkVersionV1 {\n\t\tclient, err := onepanelsdk.NewClient(serverUrl, apiKey)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif skipTlsVerify {\n\t\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t\t}\n\n\t\treturn client, nil\n\t} else if apiVersion == sdkVersionV2 {\n\t\tvar client *onepanelsdk2.Client\n\t\tvar err error\n\n\t\tif nodeName == \"\" {\n\t\t\tclient, err = onepanelsdk2.NewClient(serverUrl, apiKey)\n\t\t} else {\n\t\t\tclient, err = onepanelsdk2.NewClientWithNode(serverUrl, apiKey, nodeName)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif skipTlsVerify {\n\t\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t\t}\n\n\t\treturn client, nil\n\t}\n\n\treturn nil, errors.New(\"1panel: invalid api version\")\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/1panel/1panel_test.go",
    "content": "package onepanelssl_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/1panel\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfApiVersion    string\n\tfApiKey        string\n)\n\nfunc init() {\n\targsPrefix := \"1PANEL_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiVersion, argsPrefix+\"APIVERSION\", \"v1\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./1panel_test.go -args \\\n\t--1PANEL_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--1PANEL_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--1PANEL_SERVERURL=\"http://127.0.0.1:20410\" \\\n\t--1PANEL_APIVERSION=\"v1\" \\\n\t--1PANEL_APIKEY=\"your-api-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APIVERSION: %v\", fApiVersion),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tServerUrl:  fServerUrl,\n\t\t\tApiVersion: fApiVersion,\n\t\t\tApiKey:     fApiKey,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/aliyun-cas/aliyun_cas.go",
    "content": "package aliyuncas\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\talicas \"github.com/alibabacloud-go/cas-20200407/v4/client\"\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *internal.CasClient\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 查询证书列表，避免重复上传\n\t// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-listusercertificateorder\n\t// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-getusercertificatedetail\n\tlistUserCertificateOrderPage := 1\n\tlistUserCertificateOrderLimit := 50\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistUserCertificateOrderReq := &alicas.ListUserCertificateOrderRequest{\n\t\t\tResourceGroupId: lo.EmptyableToPtr(c.config.ResourceGroupId),\n\t\t\tCurrentPage:     tea.Int64(int64(listUserCertificateOrderPage)),\n\t\t\tShowSize:        tea.Int64(int64(listUserCertificateOrderLimit)),\n\t\t\tOrderType:       tea.String(\"CERT\"),\n\t\t}\n\t\tlistUserCertificateOrderResp, err := c.sdkClient.ListUserCertificateOrderWithContext(ctx, listUserCertificateOrderReq, &dara.RuntimeOptions{})\n\t\tc.logger.Debug(\"sdk request 'cas.ListUserCertificateOrder'\", slog.Any(\"request\", listUserCertificateOrderReq), slog.Any(\"response\", listUserCertificateOrderResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cas.ListUserCertificateOrder': %w\", err)\n\t\t}\n\n\t\tif listUserCertificateOrderResp.Body == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, certItem := range listUserCertificateOrderResp.Body.CertificateOrderList {\n\t\t\t// 对比证书通用名称\n\t\t\tif !strings.EqualFold(certX509.Subject.CommonName, tea.StringValue(certItem.CommonName)) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书序列号\n\t\t\t// 注意阿里云 CAS 会在序列号前补零，需去除后再比较\n\t\t\toldCertSN := strings.TrimLeft(tea.StringValue(certItem.SerialNo), \"0\")\n\t\t\tnewCertSN := strings.TrimLeft(certX509.SerialNumber.Text(16), \"0\")\n\t\t\tif !strings.EqualFold(newCertSN, oldCertSN) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书内容\n\t\t\tgetUserCertificateDetailReq := &alicas.GetUserCertificateDetailRequest{\n\t\t\t\tCertId: certItem.CertificateId,\n\t\t\t}\n\t\t\tgetUserCertificateDetailResp, err := c.sdkClient.GetUserCertificateDetailWithContext(ctx, getUserCertificateDetailReq, &dara.RuntimeOptions{})\n\t\t\tc.logger.Debug(\"sdk request 'cas.GetUserCertificateDetail'\", slog.Any(\"request\", getUserCertificateDetailReq), slog.Any(\"response\", getUserCertificateDetailResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cas.GetUserCertificateDetail': %w\", err)\n\t\t\t} else {\n\t\t\t\tif !xcert.EqualCertificatesFromPEM(certPEM, tea.StringValue(getUserCertificateDetailResp.Body.Cert)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   fmt.Sprintf(\"%d\", tea.Int64Value(certItem.CertificateId)),\n\t\t\t\tCertName: *certItem.Name,\n\t\t\t\tExtendedData: map[string]any{\n\t\t\t\t\t\"InstanceId\":     tea.StringValue(getUserCertificateDetailResp.Body.InstanceId),\n\t\t\t\t\t\"CertIdentifier\": tea.StringValue(getUserCertificateDetailResp.Body.CertIdentifier),\n\t\t\t\t},\n\t\t\t}, nil\n\t\t}\n\n\t\tif len(listUserCertificateOrderResp.Body.CertificateOrderList) < listUserCertificateOrderLimit {\n\t\t\tbreak\n\t\t}\n\n\t\tlistUserCertificateOrderPage++\n\t}\n\n\t// 生成新证书名（需符合阿里云命名规则）\n\tcertName := fmt.Sprintf(\"certimate_%d\", time.Now().UnixMilli())\n\n\t// 上传新证书\n\t// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-uploadusercertificate\n\tuploadUserCertificateReq := &alicas.UploadUserCertificateRequest{\n\t\tResourceGroupId: lo.EmptyableToPtr(c.config.ResourceGroupId),\n\t\tName:            tea.String(certName),\n\t\tCert:            tea.String(certPEM),\n\t\tKey:             tea.String(privkeyPEM),\n\t}\n\tuploadUserCertificateResp, err := c.sdkClient.UploadUserCertificateWithContext(ctx, uploadUserCertificateReq, &dara.RuntimeOptions{})\n\tc.logger.Debug(\"sdk request 'cas.UploadUserCertificate'\", slog.Any(\"request\", uploadUserCertificateReq), slog.Any(\"response\", uploadUserCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cas.UploadUserCertificate': %w\", err)\n\t}\n\n\t// 获取证书详情\n\t// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-getusercertificatedetail\n\tgetUserCertificateDetailReq := &alicas.GetUserCertificateDetailRequest{\n\t\tCertId:     uploadUserCertificateResp.Body.CertId,\n\t\tCertFilter: tea.Bool(true),\n\t}\n\tgetUserCertificateDetailResp, err := c.sdkClient.GetUserCertificateDetailWithContext(ctx, getUserCertificateDetailReq, &dara.RuntimeOptions{})\n\tc.logger.Debug(\"sdk request 'cas.GetUserCertificateDetail'\", slog.Any(\"request\", getUserCertificateDetailReq), slog.Any(\"response\", getUserCertificateDetailResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cas.GetUserCertificateDetail': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   fmt.Sprintf(\"%d\", tea.Int64Value(getUserCertificateDetailResp.Body.Id)),\n\t\tCertName: certName,\n\t\tExtendedData: map[string]any{\n\t\t\t\"InstanceId\":     tea.StringValue(getUserCertificateDetailResp.Body.InstanceId),\n\t\t\t\"CertIdentifier\": tea.StringValue(getUserCertificateDetailResp.Body.CertIdentifier),\n\t\t},\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.CasClient, error) {\n\t// 接入点一览 https://api.aliyun.com/product/cas\n\tvar endpoint string\n\tswitch region {\n\tcase \"\", \"cn-hangzhou\":\n\t\tendpoint = \"cas.aliyuncs.com\"\n\tdefault:\n\t\tendpoint = fmt.Sprintf(\"cas.%s.aliyuncs.com\", region)\n\t}\n\n\tconfig := &aliopen.Config{\n\t\tEndpoint:        tea.String(endpoint),\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t}\n\n\tclient, err := internal.NewCasClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/aliyun-cas/aliyun_cas_test.go",
    "content": "package aliyuncas_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNCAS_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_cas_test.go -args \\\n\t--ALIYUNCAS_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNCAS_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNCAS_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNCAS_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNCAS_REGION=\"cn-hangzhou\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegion:          fRegion,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/aliyun-cas/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\n\talicas \"github.com/alibabacloud-go/cas-20200407/v4/client\"\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\topenapiutil \"github.com/alibabacloud-go/darabonba-openapi/v2/utils\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/cas-20200407/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype CasClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewCasClient(config *openapiutil.Config) (*CasClient, error) {\n\tclient := new(CasClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *CasClient) Init(config *openapiutil.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *CasClient) GetUserCertificateDetailWithContext(ctx context.Context, request *alicas.GetUserCertificateDetailRequest, runtime *dara.RuntimeOptions) (_result *alicas.GetUserCertificateDetailResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.CertFilter) {\n\t\tquery[\"CertFilter\"] = request.CertFilter\n\t}\n\n\tif !dara.IsNil(request.CertId) {\n\t\tquery[\"CertId\"] = request.CertId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"GetUserCertificateDetail\"),\n\t\tVersion:     dara.String(\"2020-04-07\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alicas.GetUserCertificateDetailResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *CasClient) ListUserCertificateOrderWithContext(ctx context.Context, request *alicas.ListUserCertificateOrderRequest, runtime *dara.RuntimeOptions) (_result *alicas.ListUserCertificateOrderResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.CurrentPage) {\n\t\tquery[\"CurrentPage\"] = request.CurrentPage\n\t}\n\n\tif !dara.IsNil(request.Keyword) {\n\t\tquery[\"Keyword\"] = request.Keyword\n\t}\n\n\tif !dara.IsNil(request.OrderType) {\n\t\tquery[\"OrderType\"] = request.OrderType\n\t}\n\n\tif !dara.IsNil(request.ResourceGroupId) {\n\t\tquery[\"ResourceGroupId\"] = request.ResourceGroupId\n\t}\n\n\tif !dara.IsNil(request.ShowSize) {\n\t\tquery[\"ShowSize\"] = request.ShowSize\n\t}\n\n\tif !dara.IsNil(request.Status) {\n\t\tquery[\"Status\"] = request.Status\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"ListUserCertificateOrder\"),\n\t\tVersion:     dara.String(\"2020-04-07\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alicas.ListUserCertificateOrderResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *CasClient) UploadUserCertificateWithContext(ctx context.Context, request *alicas.UploadUserCertificateRequest, runtime *dara.RuntimeOptions) (_result *alicas.UploadUserCertificateResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.Cert) {\n\t\tquery[\"Cert\"] = request.Cert\n\t}\n\n\tif !dara.IsNil(request.EncryptCert) {\n\t\tquery[\"EncryptCert\"] = request.EncryptCert\n\t}\n\n\tif !dara.IsNil(request.EncryptPrivateKey) {\n\t\tquery[\"EncryptPrivateKey\"] = request.EncryptPrivateKey\n\t}\n\n\tif !dara.IsNil(request.Key) {\n\t\tquery[\"Key\"] = request.Key\n\t}\n\n\tif !dara.IsNil(request.Name) {\n\t\tquery[\"Name\"] = request.Name\n\t}\n\n\tif !dara.IsNil(request.ResourceGroupId) {\n\t\tquery[\"ResourceGroupId\"] = request.ResourceGroupId\n\t}\n\n\tif !dara.IsNil(request.SignCert) {\n\t\tquery[\"SignCert\"] = request.SignCert\n\t}\n\n\tif !dara.IsNil(request.SignPrivateKey) {\n\t\tquery[\"SignPrivateKey\"] = request.SignPrivateKey\n\t}\n\n\tif !dara.IsNil(request.Tags) {\n\t\tquery[\"Tags\"] = request.Tags\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"UploadUserCertificate\"),\n\t\tVersion:     dara.String(\"2020-04-07\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alicas.UploadUserCertificateResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/aliyun-slb/aliyun_slb.go",
    "content": "package aliyunslb\n\nimport (\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\talislb \"github.com/alibabacloud-go/slb-20140515/v4/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-slb/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *internal.SlbClient\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 查询证书列表，避免重复上传\n\t// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeservercertificates\n\tdescribeServerCertificatesReq := &alislb.DescribeServerCertificatesRequest{\n\t\tResourceGroupId: lo.EmptyableToPtr(c.config.ResourceGroupId),\n\t\tRegionId:        tea.String(c.config.Region),\n\t}\n\tdescribeServerCertificatesResp, err := c.sdkClient.DescribeServerCertificatesWithContext(ctx, describeServerCertificatesReq, &dara.RuntimeOptions{})\n\tc.logger.Debug(\"sdk request 'slb.DescribeServerCertificates'\", slog.Any(\"request\", describeServerCertificatesReq), slog.Any(\"response\", describeServerCertificatesResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'slb.DescribeServerCertificates': %w\", err)\n\t}\n\n\tif describeServerCertificatesResp.Body.ServerCertificates != nil && describeServerCertificatesResp.Body.ServerCertificates.ServerCertificate != nil {\n\t\tfingerprintSha256 := sha256.Sum256(certX509.Raw)\n\t\tfingerprintSha256Hex := hex.EncodeToString(fingerprintSha256[:])\n\t\tfingerprintSha1 := sha1.Sum(certX509.Raw)\n\t\tfingerprintSha1Hex := hex.EncodeToString(fingerprintSha1[:])\n\t\tfor _, certItem := range describeServerCertificatesResp.Body.ServerCertificates.ServerCertificate {\n\t\t\tif tea.Int32Value(certItem.IsAliCloudCertificate) != 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书通用名称\n\t\t\tif !strings.EqualFold(certX509.Subject.CommonName, tea.StringValue(certItem.CommonName)) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书 SHA-1 或 SHA-256 摘要\n\t\t\toldFingerprint := strings.ReplaceAll(tea.StringValue(certItem.Fingerprint), \":\", \"\")\n\t\t\tif !strings.EqualFold(fingerprintSha256Hex, oldFingerprint) && !strings.EqualFold(fingerprintSha1Hex, oldFingerprint) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   *certItem.ServerCertificateId,\n\t\t\t\tCertName: *certItem.ServerCertificateName,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\t// 生成新证书名（需符合阿里云命名规则）\n\tcertName := fmt.Sprintf(\"certimate_%d\", time.Now().UnixMilli())\n\n\t// 去除证书和私钥内容中的空白行，以符合阿里云 API 要求\n\t// REF: https://github.com/certimate-go/certimate/issues/326\n\tre := regexp.MustCompile(`(?m)^\\s*$\\n?`)\n\tcertPEM = strings.TrimSpace(re.ReplaceAllString(certPEM, \"\"))\n\tprivkeyPEM = strings.TrimSpace(re.ReplaceAllString(privkeyPEM, \"\"))\n\n\t// 上传新证书\n\t// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-uploadservercertificate\n\tuploadServerCertificateReq := &alislb.UploadServerCertificateRequest{\n\t\tResourceGroupId:       lo.EmptyableToPtr(c.config.ResourceGroupId),\n\t\tRegionId:              tea.String(c.config.Region),\n\t\tServerCertificateName: tea.String(certName),\n\t\tServerCertificate:     tea.String(certPEM),\n\t\tPrivateKey:            tea.String(privkeyPEM),\n\t}\n\tuploadServerCertificateResp, err := c.sdkClient.UploadServerCertificateWithContext(ctx, uploadServerCertificateReq, &dara.RuntimeOptions{})\n\tc.logger.Debug(\"sdk request 'slb.UploadServerCertificate'\", slog.Any(\"request\", uploadServerCertificateReq), slog.Any(\"response\", uploadServerCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'slb.UploadServerCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   *uploadServerCertificateResp.Body.ServerCertificateId,\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.SlbClient, error) {\n\t// 接入点一览 https://api.aliyun.com/product/Slb\n\tvar endpoint string\n\tswitch region {\n\tcase \"\",\n\t\t\"cn-hangzhou\",\n\t\t\"cn-hangzhou-finance\",\n\t\t\"cn-shanghai-finance-1\",\n\t\t\"cn-shenzhen-finance-1\":\n\t\tendpoint = \"slb.aliyuncs.com\"\n\tdefault:\n\t\tendpoint = fmt.Sprintf(\"slb.%s.aliyuncs.com\", region)\n\t}\n\n\tconfig := &aliopen.Config{\n\t\tEndpoint:        tea.String(endpoint),\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t}\n\n\tclient, err := internal.NewSlbClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/aliyun-slb/aliyun_slb_test.go",
    "content": "package aliyunslb_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-slb\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNSLB_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_slb_test.go -args \\\n\t--ALIYUNSLB_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNSLB_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNSLB_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNSLB_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNSLB_REGION=\"cn-hangzhou\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegion:          fRegion,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/aliyun-slb/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\topenapiutil \"github.com/alibabacloud-go/darabonba-openapi/v2/utils\"\n\talislb \"github.com/alibabacloud-go/slb-20140515/v4/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/slb-20140515/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype SlbClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewSlbClient(config *openapi.Config) (*SlbClient, error) {\n\tclient := new(SlbClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *SlbClient) Init(config *openapi.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *SlbClient) DescribeServerCertificatesWithContext(ctx context.Context, request *alislb.DescribeServerCertificatesRequest, runtime *dara.RuntimeOptions) (_result *alislb.DescribeServerCertificatesResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\n\tquery := map[string]interface{}{}\n\tif !dara.IsNil(request.OwnerAccount) {\n\t\tquery[\"OwnerAccount\"] = request.OwnerAccount\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !dara.IsNil(request.ResourceGroupId) {\n\t\tquery[\"ResourceGroupId\"] = request.ResourceGroupId\n\t}\n\n\tif !dara.IsNil(request.ResourceOwnerAccount) {\n\t\tquery[\"ResourceOwnerAccount\"] = request.ResourceOwnerAccount\n\t}\n\n\tif !dara.IsNil(request.ResourceOwnerId) {\n\t\tquery[\"ResourceOwnerId\"] = request.ResourceOwnerId\n\t}\n\n\tif !dara.IsNil(request.ServerCertificateId) {\n\t\tquery[\"ServerCertificateId\"] = request.ServerCertificateId\n\t}\n\n\tif !dara.IsNil(request.Tag) {\n\t\tquery[\"Tag\"] = request.Tag\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DescribeServerCertificates\"),\n\t\tVersion:     dara.String(\"2014-05-15\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alislb.DescribeServerCertificatesResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *SlbClient) UploadServerCertificateWithContext(ctx context.Context, request *alislb.UploadServerCertificateRequest, runtime *dara.RuntimeOptions) (_result *alislb.UploadServerCertificateResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\n\tquery := map[string]interface{}{}\n\tif !dara.IsNil(request.AliCloudCertificateId) {\n\t\tquery[\"AliCloudCertificateId\"] = request.AliCloudCertificateId\n\t}\n\n\tif !dara.IsNil(request.AliCloudCertificateName) {\n\t\tquery[\"AliCloudCertificateName\"] = request.AliCloudCertificateName\n\t}\n\n\tif !dara.IsNil(request.AliCloudCertificateRegionId) {\n\t\tquery[\"AliCloudCertificateRegionId\"] = request.AliCloudCertificateRegionId\n\t}\n\n\tif !dara.IsNil(request.OwnerAccount) {\n\t\tquery[\"OwnerAccount\"] = request.OwnerAccount\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.PrivateKey) {\n\t\tquery[\"PrivateKey\"] = request.PrivateKey\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !dara.IsNil(request.ResourceGroupId) {\n\t\tquery[\"ResourceGroupId\"] = request.ResourceGroupId\n\t}\n\n\tif !dara.IsNil(request.ResourceOwnerAccount) {\n\t\tquery[\"ResourceOwnerAccount\"] = request.ResourceOwnerAccount\n\t}\n\n\tif !dara.IsNil(request.ResourceOwnerId) {\n\t\tquery[\"ResourceOwnerId\"] = request.ResourceOwnerId\n\t}\n\n\tif !dara.IsNil(request.ServerCertificate) {\n\t\tquery[\"ServerCertificate\"] = request.ServerCertificate\n\t}\n\n\tif !dara.IsNil(request.ServerCertificateName) {\n\t\tquery[\"ServerCertificateName\"] = request.ServerCertificateName\n\t}\n\n\tif !dara.IsNil(request.Tag) {\n\t\tquery[\"Tag\"] = request.Tag\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"UploadServerCertificate\"),\n\t\tVersion:     dara.String(\"2014-05-15\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alislb.UploadServerCertificateResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/aws-acm/aws_acm.go",
    "content": "package awsacm\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\taws \"github.com/aws/aws-sdk-go-v2/aws\"\n\tawscfg \"github.com/aws/aws-sdk-go-v2/config\"\n\tawscred \"github.com/aws/aws-sdk-go-v2/credentials\"\n\tawsacm \"github.com/aws/aws-sdk-go-v2/service/acm\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// AWS AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// AWS SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// AWS 区域。\n\tRegion string `json:\"region\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *awsacm.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 提取服务器证书和中间证书\n\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to extract certs: %w\", err)\n\t}\n\n\t// 获取证书列表，避免重复上传\n\t// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ListCertificates.html\n\t// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_GetCertificate.html\n\tlistCertificatesNextToken := (*string)(nil)\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistCertificatesReq := &awsacm.ListCertificatesInput{\n\t\t\tNextToken: listCertificatesNextToken,\n\t\t\tMaxItems:  aws.Int32(1000),\n\t\t}\n\t\tlistCertificatesResp, err := c.sdkClient.ListCertificates(ctx, listCertificatesReq)\n\t\tc.logger.Debug(\"sdk request 'acm.ListCertificates'\", slog.Any(\"request\", listCertificatesReq), slog.Any(\"response\", listCertificatesResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'acm.ListCertificates': %w\", err)\n\t\t}\n\n\t\tfor _, certItem := range listCertificatesResp.CertificateSummaryList {\n\t\t\t// 对比证书有效期\n\t\t\tif certItem.NotBefore == nil || !certItem.NotBefore.Equal(certX509.NotBefore) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif certItem.NotAfter == nil || !certItem.NotAfter.Equal(certX509.NotAfter) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书多域名\n\t\t\tif !strings.EqualFold(strings.Join(certX509.DNSNames, \",\"), strings.Join(certItem.SubjectAlternativeNameSummaries, \",\")) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书内容\n\t\t\tgetCertificateReq := &awsacm.GetCertificateInput{\n\t\t\t\tCertificateArn: certItem.CertificateArn,\n\t\t\t}\n\t\t\tgetCertificateResp, err := c.sdkClient.GetCertificate(ctx, getCertificateReq)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'acm.GetCertificate': %w\", err)\n\t\t\t} else {\n\t\t\t\tif !xcert.EqualCertificatesFromPEM(certPEM, aws.ToString(getCertificateResp.Certificate)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId: *certItem.CertificateArn,\n\t\t\t}, nil\n\t\t}\n\n\t\tif len(listCertificatesResp.CertificateSummaryList) == 0 || listCertificatesResp.NextToken == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tlistCertificatesNextToken = listCertificatesResp.NextToken\n\t}\n\n\t// 导入证书\n\t// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ImportCertificate.html\n\timportCertificateReq := &awsacm.ImportCertificateInput{\n\t\tCertificate:      ([]byte)(serverCertPEM),\n\t\tCertificateChain: ([]byte)(intermediaCertPEM),\n\t\tPrivateKey:       ([]byte)(privkeyPEM),\n\t}\n\timportCertificateResp, err := c.sdkClient.ImportCertificate(ctx, importCertificateReq)\n\tc.logger.Debug(\"sdk request 'acm.ImportCertificate'\", slog.Any(\"request\", importCertificateReq), slog.Any(\"response\", importCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'acm.ImportCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId: aws.ToString(importCertificateResp.CertificateArn),\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\t// 提取服务器证书和中间证书\n\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to extract certs: %w\", err)\n\t}\n\n\t// 导入证书\n\t// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ImportCertificate.html\n\timportCertificateReq := &awsacm.ImportCertificateInput{\n\t\tCertificateArn:   aws.String(certIdOrName),\n\t\tCertificate:      ([]byte)(serverCertPEM),\n\t\tCertificateChain: ([]byte)(intermediaCertPEM),\n\t\tPrivateKey:       ([]byte)(privkeyPEM),\n\t}\n\timportCertificateResp, err := c.sdkClient.ImportCertificate(ctx, importCertificateReq)\n\tc.logger.Debug(\"sdk request 'acm.ImportCertificate'\", slog.Any(\"request\", importCertificateReq), slog.Any(\"response\", importCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'acm.ImportCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.OperateResult{}, nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey, region string) (*awsacm.Client, error) {\n\tcfg, err := awscfg.LoadDefaultConfig(context.Background())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := awsacm.NewFromConfig(cfg, func(o *awsacm.Options) {\n\t\to.Region = region\n\t\to.Credentials = aws.NewCredentialsCache(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, \"\"))\n\t})\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/aws-iam/aws_iam.go",
    "content": "package awsiam\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\taws \"github.com/aws/aws-sdk-go-v2/aws\"\n\tawscfg \"github.com/aws/aws-sdk-go-v2/config\"\n\tawscred \"github.com/aws/aws-sdk-go-v2/credentials\"\n\tawsiam \"github.com/aws/aws-sdk-go-v2/service/iam\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// AWS AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// AWS SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// AWS 区域。\n\tRegion string `json:\"region\"`\n\t// IAM 证书路径。\n\t// 选填。\n\tCertificatePath string `json:\"certificatePath,omitempty\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *awsiam.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 提取服务器证书和中间证书\n\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to extract certs: %w\", err)\n\t}\n\n\t// 获取证书列表，避免重复上传\n\t// REF: https://docs.aws.amazon.com/en_us/IAM/latest/APIReference/API_ListServerCertificates.html\n\t// REF: https://docs.aws.amazon.com/en_us/IAM/latest/APIReference/API_GetServerCertificate.html\n\tlistServerCertificatesMarker := (*string)(nil)\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistServerCertificatesReq := &awsiam.ListServerCertificatesInput{\n\t\t\tMarker:   listServerCertificatesMarker,\n\t\t\tMaxItems: aws.Int32(1000),\n\t\t}\n\t\tif c.config.CertificatePath != \"\" {\n\t\t\tlistServerCertificatesReq.PathPrefix = aws.String(c.config.CertificatePath)\n\t\t}\n\t\tlistServerCertificatesResp, err := c.sdkClient.ListServerCertificates(ctx, listServerCertificatesReq)\n\t\tc.logger.Debug(\"sdk request 'iam.ListServerCertificates'\", slog.Any(\"request\", listServerCertificatesReq), slog.Any(\"response\", listServerCertificatesResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'iam.ListServerCertificates': %w\", err)\n\t\t}\n\n\t\tfor _, certItem := range listServerCertificatesResp.ServerCertificateMetadataList {\n\t\t\t// 对比证书路径\n\t\t\tif c.config.CertificatePath != \"\" && aws.ToString(certItem.Path) != c.config.CertificatePath {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书有效期\n\t\t\tif certItem.Expiration == nil || !certItem.Expiration.Equal(certX509.NotAfter) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书内容\n\t\t\tgetServerCertificateReq := &awsiam.GetServerCertificateInput{\n\t\t\t\tServerCertificateName: certItem.ServerCertificateName,\n\t\t\t}\n\t\t\tgetServerCertificateResp, err := c.sdkClient.GetServerCertificate(ctx, getServerCertificateReq)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'iam.GetServerCertificate': %w\", err)\n\t\t\t} else {\n\t\t\t\tif !xcert.EqualCertificatesFromPEM(certPEM, aws.ToString(getServerCertificateResp.ServerCertificate.CertificateBody)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   aws.ToString(certItem.ServerCertificateId),\n\t\t\t\tCertName: aws.ToString(certItem.ServerCertificateName),\n\t\t\t}, nil\n\t\t}\n\n\t\tif len(listServerCertificatesResp.ServerCertificateMetadataList) == 0 || listServerCertificatesResp.Marker == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tlistServerCertificatesMarker = listServerCertificatesResp.Marker\n\t}\n\n\t// 生成新证书名（需符合 AWS IAM 命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 导入证书\n\t// REF: https://docs.aws.amazon.com/en_us/IAM/latest/APIReference/API_UploadServerCertificate.html\n\tuploadServerCertificateReq := &awsiam.UploadServerCertificateInput{\n\t\tServerCertificateName: aws.String(certName),\n\t\tPath:                  aws.String(c.config.CertificatePath),\n\t\tCertificateBody:       aws.String(serverCertPEM),\n\t\tCertificateChain:      aws.String(intermediaCertPEM),\n\t\tPrivateKey:            aws.String(privkeyPEM),\n\t}\n\tif c.config.CertificatePath == \"\" {\n\t\tuploadServerCertificateReq.Path = aws.String(\"/\")\n\t}\n\tuploadServerCertificateResp, err := c.sdkClient.UploadServerCertificate(ctx, uploadServerCertificateReq)\n\tc.logger.Debug(\"sdk request 'iam.UploadServerCertificate'\", slog.Any(\"request\", uploadServerCertificateReq), slog.Any(\"response\", uploadServerCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'iam.UploadServerCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   aws.ToString(uploadServerCertificateResp.ServerCertificateMetadata.ServerCertificateId),\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey, region string) (*awsiam.Client, error) {\n\tcfg, err := awscfg.LoadDefaultConfig(context.Background())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := awsiam.NewFromConfig(cfg, func(o *awsiam.Options) {\n\t\to.Region = region\n\t\to.Credentials = aws.NewCredentialsCache(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, \"\"))\n\t})\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/azure-keyvault/azure_keyvault.go",
    "content": "package azurekeyvault\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/to\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azidentity\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tazenv \"github.com/certimate-go/certimate/pkg/sdk3rd/azure/env\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// Azure TenantId。\n\tTenantId string `json:\"tenantId\"`\n\t// Azure ClientId。\n\tClientId string `json:\"clientId\"`\n\t// Azure ClientSecret。\n\tClientSecret string `json:\"clientSecret\"`\n\t// Azure 主权云环境。\n\tCloudName string `json:\"cloudName,omitempty\"`\n\t// Key Vault 名称。\n\tKeyVaultName string `json:\"keyvaultName\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *azcertificates.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.CloudName, config.TenantId, config.ClientId, config.ClientSecret, config.KeyVaultName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 生成 Azure 业务参数\n\tcertCN := certX509.Subject.CommonName\n\tcertSN := certX509.SerialNumber.Text(16)\n\n\t// 获取证书列表，避免重复上传\n\t// REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/get-certificates/get-certificates\n\tlistCertificatesPager := c.sdkClient.NewListCertificatePropertiesPager(&azcertificates.ListCertificatePropertiesOptions{})\n\tfor listCertificatesPager.More() {\n\t\tpage, err := listCertificatesPager.NextPage(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'keyvault.GetCertificates': %w\", err)\n\t\t}\n\n\t\tfor _, certItem := range page.Value {\n\t\t\t// 对比证书有效期\n\t\t\tif certItem.Attributes == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif certItem.Attributes.NotBefore == nil || !certItem.Attributes.NotBefore.Equal(certX509.NotBefore) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif certItem.Attributes.Expires == nil || !certItem.Attributes.Expires.Equal(certX509.NotAfter) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比 Tag 中的通用名称\n\t\t\tif v, ok := certItem.Tags[kvTagCertCN]; !ok || v == nil {\n\t\t\t\tcontinue\n\t\t\t} else if *v != certCN {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比 Tag 中的序列号\n\t\t\tif v, ok := certItem.Tags[kvTagCertSN]; !ok || v == nil {\n\t\t\t\tcontinue\n\t\t\t} else if *v != certSN {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书内容\n\t\t\tgetCertificateResp, err := c.sdkClient.GetCertificate(ctx, certItem.ID.Name(), certItem.ID.Version(), nil)\n\t\t\tc.logger.Debug(\"sdk request 'keyvault.GetCertificate'\", slog.String(\"request.certificateName\", certItem.ID.Name()), slog.String(\"request.certificateVersion\", certItem.ID.Version()), slog.Any(\"response\", getCertificateResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'keyvault.GetCertificate': %w\", err)\n\t\t\t} else {\n\t\t\t\tif !xcert.EqualCertificatesFromPEM(certPEM, string(getCertificateResp.CER)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   string(*certItem.ID),\n\t\t\t\tCertName: certItem.ID.Name(),\n\t\t\t}, nil\n\t\t}\n\t}\n\n\t// 生成新证书名（需符合 Azure 命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// Azure Key Vault 不支持导入带有 Certificate Chain 的 PEM 证书。\n\t// Issue Link: https://github.com/Azure/azure-cli/issues/19017\n\t// 暂时的解决方法是，将 PEM 证书转换成 PFX 格式，然后再导入。\n\tcertPFX, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, \"\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to transform certificate from PEM to PFX: %w\", err)\n\t}\n\n\t// 导入证书\n\t// REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/import-certificate/import-certificate\n\timportCertificateParams := azcertificates.ImportCertificateParameters{\n\t\tBase64EncodedCertificate: to.Ptr(base64.StdEncoding.EncodeToString(certPFX)),\n\t\tCertificatePolicy: &azcertificates.CertificatePolicy{\n\t\t\tSecretProperties: &azcertificates.SecretProperties{\n\t\t\t\tContentType: to.Ptr(\"application/x-pkcs12\"),\n\t\t\t},\n\t\t},\n\t\tTags: map[string]*string{\n\t\t\tkvTagCertCN: to.Ptr(certCN),\n\t\t\tkvTagCertSN: to.Ptr(certSN),\n\t\t},\n\t}\n\timportCertificateResp, err := c.sdkClient.ImportCertificate(ctx, certName, importCertificateParams, nil)\n\tc.logger.Debug(\"sdk request 'keyvault.ImportCertificate'\", slog.String(\"request.certificateName\", certName), slog.Any(\"request.parameters\", importCertificateParams), slog.Any(\"response\", importCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'keyvault.ImportCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   string(*importCertificateResp.ID),\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 转换证书格式\n\tcertPFX, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, \"\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to transform certificate from PEM to PFX: %w\", err)\n\t}\n\n\t// 获取证书\n\t// REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/get-certificate/get-certificate\n\tgetCertificateResp, err := c.sdkClient.GetCertificate(ctx, certIdOrName, \"\", nil)\n\tc.logger.Debug(\"sdk request 'keyvault.GetCertificate'\", slog.String(\"request.certificateName\", certIdOrName), slog.Any(\"response\", getCertificateResp))\n\tif err != nil {\n\t\tvar respErr *azcore.ResponseError\n\t\tif !errors.As(err, &respErr) || (respErr.ErrorCode != \"ResourceNotFound\" && respErr.ErrorCode != \"CertificateNotFound\") {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'keyvault.GetCertificate': %w\", err)\n\t\t}\n\t} else {\n\t\t// 如果已存在相同证书，直接返回\n\t\tif xcert.EqualCertificatesFromPEM(certPEM, string(getCertificateResp.CER)) {\n\t\t\treturn &certmgr.OperateResult{}, nil\n\t\t}\n\t}\n\n\t// 导入证书\n\t// REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/import-certificate/import-certificate\n\timportCertificateParams := azcertificates.ImportCertificateParameters{\n\t\tBase64EncodedCertificate: to.Ptr(base64.StdEncoding.EncodeToString(certPFX)),\n\t\tCertificatePolicy: &azcertificates.CertificatePolicy{\n\t\t\tSecretProperties: &azcertificates.SecretProperties{\n\t\t\t\tContentType: to.Ptr(\"application/x-pkcs12\"),\n\t\t\t},\n\t\t},\n\t\tTags: map[string]*string{\n\t\t\tkvTagCertCN: to.Ptr(certX509.Subject.CommonName),\n\t\t\tkvTagCertSN: to.Ptr(certX509.SerialNumber.Text(16)),\n\t\t},\n\t}\n\timportCertificateResp, err := c.sdkClient.ImportCertificate(ctx, certIdOrName, importCertificateParams, nil)\n\tc.logger.Debug(\"sdk request 'keyvault.ImportCertificate'\", slog.String(\"request.certificateName\", certIdOrName), slog.Any(\"request.parameters\", importCertificateParams), slog.Any(\"response\", importCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'keyvault.ImportCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.OperateResult{}, nil\n}\n\nconst (\n\tkvTagCertCN = \"certimate/cert-cn\"\n\tkvTagCertSN = \"certimate/cert-sn\"\n)\n\nfunc createSDKClient(cloudName, tenantId, clientId, clientSecret, keyvaultName string) (*azcertificates.Client, error) {\n\tenv, err := azenv.GetCloudEnvConfiguration(cloudName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclientOptions := azcore.ClientOptions{Cloud: env}\n\tcredential, err := azidentity.NewClientSecretCredential(tenantId, clientId, clientSecret,\n\t\t&azidentity.ClientSecretCredentialOptions{ClientOptions: clientOptions})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tendpoint := fmt.Sprintf(\"https://%s.vault.azure.net\", keyvaultName)\n\tif azenv.IsUSGovernmentEnv(cloudName) {\n\t\tendpoint = fmt.Sprintf(\"https://%s.vault.usgovcloudapi.net\", keyvaultName)\n\t} else if azenv.IsChinaEnv(cloudName) {\n\t\tendpoint = fmt.Sprintf(\"https://%s.vault.azure.cn\", keyvaultName)\n\t}\n\n\tclient, err := azcertificates.NewClient(endpoint, credential, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/azure-keyvault/azure_keyvault_test.go",
    "content": "package azurekeyvault_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/azure-keyvault\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfTenantId      string\n\tfClientId      string\n\tfClientSecret  string\n\tfCloudName     string\n\tfKeyVaultName  string\n)\n\nfunc init() {\n\targsPrefix := \"AZUREKEYVAULT_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fTenantId, argsPrefix+\"TENANTID\", \"\", \"\")\n\tflag.StringVar(&fClientId, argsPrefix+\"CLIENTID\", \"\", \"\")\n\tflag.StringVar(&fClientSecret, argsPrefix+\"CLIENTSECRET\", \"\", \"\")\n\tflag.StringVar(&fCloudName, argsPrefix+\"CLOUDNAME\", \"\", \"\")\n\tflag.StringVar(&fKeyVaultName, argsPrefix+\"KEYVAULTNAME\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./azure_keyvault_test.go -args \\\n\t--AZUREKEYVAULT_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--AZUREKEYVAULT_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--AZUREKEYVAULT_TENANTID=\"your-tenant-id\" \\\n\t--AZUREKEYVAULT_CLIENTID=\"your-app-registration-client-id\" \\\n\t--AZUREKEYVAULT_CLIENTSECRET=\"your-app-registration-client-secret\" \\\n\t--AZUREKEYVAULT_CLOUDNAME=\"china\" \\\n\t--AZUREKEYVAULT_KEYVAULTNAME=\"your-keyvault-name\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"TENANTID: %v\", fTenantId),\n\t\t\tfmt.Sprintf(\"CLIENTID: %v\", fClientId),\n\t\t\tfmt.Sprintf(\"CLIENTSECRET: %v\", fClientSecret),\n\t\t\tfmt.Sprintf(\"CLOUDNAME: %v\", fCloudName),\n\t\t\tfmt.Sprintf(\"KEYVAULTNAME: %v\", fKeyVaultName),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tTenantId:     fTenantId,\n\t\t\tClientId:     fClientId,\n\t\t\tClientSecret: fClientSecret,\n\t\t\tCloudName:    fCloudName,\n\t\t\tKeyVaultName: fKeyVaultName,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/baiducloud-cert/baiducloud_cert.go",
    "content": "package baiducloudcert\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tbaiducert \"github.com/certimate-go/certimate/pkg/sdk3rd/baiducloud/cert\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 百度智能云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 百度智能云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *baiducert.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 查看证书列表\n\t// REF: https://cloud.baidu.com/doc/Reference/s/Gjwvz27xu#35-%E6%9F%A5%E7%9C%8B%E8%AF%81%E4%B9%A6%E5%88%97%E8%A1%A8%E8%AF%A6%E6%83%85\n\tlistCertDetail, err := c.sdkClient.ListCertDetail()\n\tc.logger.Debug(\"sdk request 'cert.ListCertDetail'\", slog.Any(\"response\", listCertDetail))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cert.ListCertDetail': %w\", err)\n\t} else {\n\t\tfor _, certItem := range listCertDetail.Certs {\n\t\t\t// 对比证书通用名称\n\t\t\tif !strings.EqualFold(certX509.Subject.CommonName, certItem.CertCommonName) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书有效期\n\t\t\toldCertNotBefore, _ := time.Parse(\"2006-01-02T15:04:05Z\", certItem.CertStartTime)\n\t\t\toldCertNotAfter, _ := time.Parse(\"2006-01-02T15:04:05Z\", certItem.CertStopTime)\n\t\t\tif !certX509.NotBefore.Equal(oldCertNotBefore) || !certX509.NotAfter.Equal(oldCertNotAfter) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书多域名\n\t\t\tif certItem.CertDNSNames != strings.Join(certX509.DNSNames, \",\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书内容\n\t\t\tgetCertDetailResp, err := c.sdkClient.GetCertRawData(certItem.CertId)\n\t\t\tc.logger.Debug(\"sdk request 'cert.GetCertRawData'\", slog.Any(\"certId\", certItem.CertId), slog.Any(\"response\", getCertDetailResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cert.GetCertRawData': %w\", err)\n\t\t\t} else {\n\t\t\t\tif !xcert.EqualCertificatesFromPEM(certPEM, getCertDetailResp.CertServerData) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   certItem.CertId,\n\t\t\t\tCertName: certItem.CertName,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\t// 创建证书\n\t// REF: https://cloud.baidu.com/doc/Reference/s/Gjwvz27xu#31-%E5%88%9B%E5%BB%BA%E8%AF%81%E4%B9%A6\n\tcreateCertReq := &baiducert.CreateCertArgs{}\n\tcreateCertReq.CertName = fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\tcreateCertReq.CertServerData = certPEM\n\tcreateCertReq.CertPrivateData = privkeyPEM\n\tcreateCertResp, err := c.sdkClient.CreateCert(createCertReq)\n\tc.logger.Debug(\"sdk request 'cert.CreateCert'\", slog.Any(\"request\", createCertReq), slog.Any(\"response\", createCertResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cert.CreateCert': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   createCertResp.CertId,\n\t\tCertName: createCertResp.CertName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey string) (*baiducert.Client, error) {\n\tclient, err := baiducert.NewClient(accessKeyId, secretAccessKey, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/baiducloud-cert/baiducloud_cert_test.go",
    "content": "package baiducloudcert_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/baiducloud-cert\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n)\n\nfunc init() {\n\targsPrefix := \"BAIDUCLOUDCERT_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./baiducloud_cert_test.go -args \\\n\t--BAIDUCLOUDCERT_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--BAIDUCLOUDCERT_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--BAIDUCLOUDCERT_ACCESSKEYID=\"your-access-key-id\" \\\n\t--BAIDUCLOUDCERT_SECRETACCESSKEY=\"your-access-key-secret\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/baishan-cdn/baishan_cdn.go",
    "content": "package baishancdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tbaishansdk \"github.com/certimate-go/certimate/pkg/sdk3rd/baishan\"\n)\n\ntype CertmgrConfig struct {\n\t// 白山云 API Token。\n\tApiToken string `json:\"apiToken\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *baishansdk.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ApiToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 生成新证书名（需符合白山云命名规则）\n\tcertName := fmt.Sprintf(\"certimate_%d\", time.Now().UnixMilli())\n\n\t// 新增证书\n\t// REF: https://portal.baishancloud.com/track/document/downloadPdf/1441\n\tcertId := \"\"\n\tuploadDomainCertificateReq := &baishansdk.UploadDomainCertificateRequest{\n\t\tName:        lo.ToPtr(certName),\n\t\tCertificate: lo.ToPtr(certPEM),\n\t\tKey:         lo.ToPtr(privkeyPEM),\n\t}\n\tuploadDomainCertificateResp, err := d.sdkClient.UploadDomainCertificateWithContext(ctx, uploadDomainCertificateReq)\n\td.logger.Debug(\"sdk request 'baishan.UploadDomainCertificate'\", slog.Any(\"request\", uploadDomainCertificateReq), slog.Any(\"response\", uploadDomainCertificateResp))\n\tif err != nil {\n\t\tif uploadDomainCertificateResp != nil {\n\t\t\tif uploadDomainCertificateResp.GetCode() == 400699 && strings.Contains(uploadDomainCertificateResp.GetMessage(), \"this certificate is exists\") {\n\t\t\t\t// 证书已存在，忽略新增证书接口错误\n\t\t\t\tre := regexp.MustCompile(`\\d+`)\n\t\t\t\tcertId = re.FindString(uploadDomainCertificateResp.GetMessage())\n\t\t\t}\n\t\t}\n\n\t\tif certId == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'baishan.SetDomainCertificate': %w\", err)\n\t\t}\n\t} else {\n\t\tcertId = uploadDomainCertificateResp.Data.CertId.String()\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   certId,\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (d *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\t// 替换证书\n\t// REF: https://portal.baishancloud.com/track/document/downloadPdf/1441\n\tuploadDomainCertificateReq := &baishansdk.UploadDomainCertificateRequest{\n\t\tCertificateId: lo.ToPtr(certIdOrName),\n\t\tName:          lo.ToPtr(fmt.Sprintf(\"certimate_%d\", time.Now().UnixMilli())),\n\t\tCertificate:   lo.ToPtr(certPEM),\n\t\tKey:           lo.ToPtr(privkeyPEM),\n\t}\n\tuploadDomainCertificateResp, err := d.sdkClient.UploadDomainCertificateWithContext(ctx, uploadDomainCertificateReq)\n\td.logger.Debug(\"sdk request 'baishan.UploadDomainCertificate'\", slog.Any(\"request\", uploadDomainCertificateReq), slog.Any(\"response\", uploadDomainCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'baishan.UploadDomainCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.OperateResult{}, nil\n}\n\nfunc createSDKClient(apiToken string) (*baishansdk.Client, error) {\n\treturn baishansdk.NewClient(apiToken)\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/baishan-cdn/baishan_cdn_test.go",
    "content": "package baishancdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/baishan-cdn\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfApiToken      string\n)\n\nfunc init() {\n\targsPrefix := \"BAISHANCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fApiToken, argsPrefix+\"APITOKEN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./baishan_cdn_test.go -args \\\n\t--BAISHANCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--BAISHANCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--BAISHANCDN_APITOKEN=\"your-api-token\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"APITOKEN: %v\", fApiToken),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tApiToken: fApiToken,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/byteplus-cdn/byteplus_cdn.go",
    "content": "package bytepluscdn\n\nimport (\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\tbytepluscdn \"github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// BytePlus AccessKey。\n\tAccessKey string `json:\"accessKey\"`\n\t// BytePlus SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *bytepluscdn.CDN\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient := bytepluscdn.NewInstance()\n\tclient.Client.SetAccessKey(config.AccessKey)\n\tclient.Client.SetSecretKey(config.SecretKey)\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 查询证书列表，避免重复上传\n\t// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-listcertinfo\n\tlistCertInfoPageNum := 1\n\tlistCertInfoPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistCertInfoReq := &bytepluscdn.ListCertInfoRequest{\n\t\t\tPageNum:  bytepluscdn.GetInt64Ptr(int64(listCertInfoPageNum)),\n\t\t\tPageSize: bytepluscdn.GetInt64Ptr(int64(listCertInfoPageSize)),\n\t\t\tSource:   bytepluscdn.GetStrPtr(\"cert_center\"),\n\t\t}\n\t\tlistCertInfoResp, err := c.sdkClient.ListCertInfo(listCertInfoReq)\n\t\tc.logger.Debug(\"sdk request 'cdn.ListCertInfo'\", slog.Any(\"request\", listCertInfoReq), slog.Any(\"response\", listCertInfoResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.ListCertInfo': %w\", err)\n\t\t}\n\n\t\tfor _, certItem := range listCertInfoResp.Result.CertInfo {\n\t\t\t// 对比证书 SHA-1 摘要\n\t\t\tfingerprintSha1 := sha1.Sum(certX509.Raw)\n\t\t\tif !strings.EqualFold(hex.EncodeToString(fingerprintSha1[:]), certItem.CertFingerprint.Sha1) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书 SHA-256 摘要\n\t\t\tfingerprintSha256 := sha256.Sum256(certX509.Raw)\n\t\t\tif !strings.EqualFold(hex.EncodeToString(fingerprintSha256[:]), certItem.CertFingerprint.Sha256) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   certItem.CertId,\n\t\t\t\tCertName: certItem.Desc,\n\t\t\t}, nil\n\t\t}\n\n\t\tif len(listCertInfoResp.Result.CertInfo) < listCertInfoPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tlistCertInfoPageNum++\n\t}\n\n\t// 生成新证书名（需符合 BytePlus 命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 上传新证书\n\t// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-addcertificate\n\taddCertificateReq := &bytepluscdn.AddCertificateRequest{\n\t\tCertificate: certPEM,\n\t\tPrivateKey:  privkeyPEM,\n\t\tSource:      bytepluscdn.GetStrPtr(\"cert_center\"),\n\t\tDesc:        bytepluscdn.GetStrPtr(certName),\n\t}\n\taddCertificateResp, err := c.sdkClient.AddCertificate(addCertificateReq)\n\tc.logger.Debug(\"sdk request 'cdn.AddCertificate'\", slog.Any(\"request\", addCertificateReq), slog.Any(\"response\", addCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.AddCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   addCertificateResp.Result.CertId,\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ctcccloud-ao/ctcccloud_ao.go",
    "content": "package ctcccloudao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tctyunao \"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/ao\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 天翼云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 天翼云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *ctyunao.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 查询用户名下证书列表，避免重复上传\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13175&data=174&isNormal=1&vid=167\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13015&data=174&isNormal=1&vid=167\n\tlistCertPage := 1\n\tlistCertPerPage := 1000\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistCertsReq := &ctyunao.ListCertsRequest{\n\t\t\tPage:      lo.ToPtr(int32(listCertPage)),\n\t\t\tPerPage:   lo.ToPtr(int32(listCertPerPage)),\n\t\t\tUsageMode: lo.ToPtr(int32(0)),\n\t\t}\n\t\tlistCertsResp, err := c.sdkClient.ListCertsWithContext(ctx, listCertsReq)\n\t\tc.logger.Debug(\"sdk request 'ao.ListCerts'\", slog.Any(\"request\", listCertsReq), slog.Any(\"response\", listCertsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'ao.ListCerts': %w\", err)\n\t\t}\n\n\t\tif listCertsResp.ReturnObj == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, certItem := range listCertsResp.ReturnObj.Results {\n\t\t\t// 对比证书通用名称\n\t\t\tif !strings.EqualFold(certX509.Subject.CommonName, certItem.CN) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书扩展名称\n\t\t\tif !slices.Equal(certX509.DNSNames, certItem.SANs) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书有效期\n\t\t\tif !certX509.NotBefore.Equal(time.Unix(certItem.IssueTime, 0).UTC()) {\n\t\t\t\tcontinue\n\t\t\t} else if !certX509.NotAfter.Equal(time.Unix(certItem.ExpiresTime, 0).UTC()) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书内容\n\t\t\tqueryCertReq := &ctyunao.QueryCertRequest{\n\t\t\t\tId: lo.ToPtr(certItem.Id),\n\t\t\t}\n\t\t\tqueryCertResp, err := c.sdkClient.QueryCertWithContext(ctx, queryCertReq)\n\t\t\tc.logger.Debug(\"sdk request 'ao.QueryCert'\", slog.Any(\"request\", queryCertReq), slog.Any(\"response\", queryCertResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'ao.QueryCert': %w\", err)\n\t\t\t} else if queryCertResp.ReturnObj != nil && queryCertResp.ReturnObj.Result != nil {\n\t\t\t\tif !xcert.EqualCertificatesFromPEM(certPEM, queryCertResp.ReturnObj.Result.Certs) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   fmt.Sprintf(\"%d\", queryCertResp.ReturnObj.Result.Id),\n\t\t\t\tCertName: queryCertResp.ReturnObj.Result.Name,\n\t\t\t}, nil\n\t\t}\n\n\t\tif len(listCertsResp.ReturnObj.Results) < listCertPerPage {\n\t\t\tbreak\n\t\t}\n\n\t\tlistCertPage++\n\t}\n\n\t// 生成新证书名（需符合天翼云命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 创建证书\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13014&data=174&isNormal=1&vid=167\n\tcreateCertReq := &ctyunao.CreateCertRequest{\n\t\tName:  lo.ToPtr(certName),\n\t\tCerts: lo.ToPtr(certPEM),\n\t\tKey:   lo.ToPtr(privkeyPEM),\n\t}\n\tcreateCertResp, err := c.sdkClient.CreateCertWithContext(ctx, createCertReq)\n\tc.logger.Debug(\"sdk request 'ao.CreateCert'\", slog.Any(\"request\", createCertReq), slog.Any(\"response\", createCertResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'ao.CreateCert': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   fmt.Sprintf(\"%d\", createCertResp.ReturnObj.Id),\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey string) (*ctyunao.Client, error) {\n\treturn ctyunao.NewClient(accessKeyId, secretAccessKey)\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ctcccloud-ao/ctcccloud_ao_test.go",
    "content": "package ctcccloudao_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-ao\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n)\n\nfunc init() {\n\targsPrefix := \"CTCCCLOUDAO_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ctcccloud_ao_test.go -args \\\n\t--CTCCCLOUDAO_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--CTCCCLOUDAO_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--CTCCCLOUDAO_ACCESSKEYID=\"your-access-key-id\" \\\n\t--CTCCCLOUDAO_SECRETACCESSKEY=\"your-secret-access-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ctcccloud-cdn/ctcccloud_cdn.go",
    "content": "package ctcccloudcdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tctyuncdn \"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/cdn\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 天翼云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 天翼云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *ctyuncdn.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 查询证书列表，避免重复上传\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=10901&data=161&isNormal=1&vid=154\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=10899&data=161&isNormal=1&vid=154\n\tqueryCertListPage := 1\n\tqueryCertListPerPage := 1000\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tqueryCertListReq := &ctyuncdn.QueryCertListRequest{\n\t\t\tPage:      lo.ToPtr(int32(queryCertListPage)),\n\t\t\tPerPage:   lo.ToPtr(int32(queryCertListPerPage)),\n\t\t\tUsageMode: lo.ToPtr(int32(0)),\n\t\t}\n\t\tqueryCertListResp, err := c.sdkClient.QueryCertListWithContext(ctx, queryCertListReq)\n\t\tc.logger.Debug(\"sdk request 'cdn.QueryCertList'\", slog.Any(\"request\", queryCertListReq), slog.Any(\"response\", queryCertListResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.QueryCertList': %w\", err)\n\t\t}\n\n\t\tif queryCertListResp.ReturnObj == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, certItem := range queryCertListResp.ReturnObj.Results {\n\t\t\t// 对比证书通用名称\n\t\t\tif !strings.EqualFold(certX509.Subject.CommonName, certItem.CN) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书扩展名称\n\t\t\tif !slices.Equal(certX509.DNSNames, certItem.SANs) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书有效期\n\t\t\tif !certX509.NotBefore.Equal(time.Unix(certItem.IssueTime, 0).UTC()) {\n\t\t\t\tcontinue\n\t\t\t} else if !certX509.NotAfter.Equal(time.Unix(certItem.ExpiresTime, 0).UTC()) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书内容\n\t\t\tqueryCertDetailReq := &ctyuncdn.QueryCertDetailRequest{\n\t\t\t\tId: lo.ToPtr(certItem.Id),\n\t\t\t}\n\t\t\tqueryCertDetailResp, err := c.sdkClient.QueryCertDetailWithContext(ctx, queryCertDetailReq)\n\t\t\tc.logger.Debug(\"sdk request 'cdn.QueryCertDetail'\", slog.Any(\"request\", queryCertDetailReq), slog.Any(\"response\", queryCertDetailResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.QueryCertDetail': %w\", err)\n\t\t\t} else if queryCertDetailResp.ReturnObj != nil && queryCertDetailResp.ReturnObj.Result != nil {\n\t\t\t\tif !xcert.EqualCertificatesFromPEM(certPEM, queryCertDetailResp.ReturnObj.Result.Certs) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   fmt.Sprintf(\"%d\", queryCertDetailResp.ReturnObj.Result.Id),\n\t\t\t\tCertName: queryCertDetailResp.ReturnObj.Result.Name,\n\t\t\t}, nil\n\t\t}\n\n\t\tif len(queryCertListResp.ReturnObj.Results) < queryCertListPerPage {\n\t\t\tbreak\n\t\t}\n\n\t\tqueryCertListPage++\n\t}\n\n\t// 生成新证书名（需符合天翼云命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 创建证书\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=10893&data=161&isNormal=1&vid=154\n\tcreateCertReq := &ctyuncdn.CreateCertRequest{\n\t\tName:  lo.ToPtr(certName),\n\t\tCerts: lo.ToPtr(certPEM),\n\t\tKey:   lo.ToPtr(privkeyPEM),\n\t}\n\tcreateCertResp, err := c.sdkClient.CreateCertWithContext(ctx, createCertReq)\n\tc.logger.Debug(\"sdk request 'cdn.CreateCert'\", slog.Any(\"request\", createCertReq), slog.Any(\"response\", createCertResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.CreateCert': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   fmt.Sprintf(\"%d\", createCertResp.ReturnObj.Id),\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey string) (*ctyuncdn.Client, error) {\n\treturn ctyuncdn.NewClient(accessKeyId, secretAccessKey)\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ctcccloud-cdn/ctcccloud_cdn_test.go",
    "content": "package ctcccloudcdn_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-cdn\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n)\n\nfunc init() {\n\targsPrefix := \"CTCCCLOUDCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ctcccloud_cdn_test.go -args \\\n\t--CTCCCLOUDCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--CTCCCLOUDCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--CTCCCLOUDCDN_ACCESSKEYID=\"your-access-key-id\" \\\n\t--CTCCCLOUDCDN_SECRETACCESSKEY=\"your-secret-access-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ctcccloud-cms/ctcccloud_cms.go",
    "content": "package ctcccloudcms\n\nimport (\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tctyuncms \"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/cms\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 天翼云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 天翼云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *ctyuncms.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 避免重复上传\n\tif upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM); err != nil {\n\t\treturn nil, err\n\t} else if upok {\n\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\treturn upres, nil\n\t}\n\n\t// 提取服务器证书和中间证书\n\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to extract certs: %w\", err)\n\t}\n\n\t// 生成新证书名（需符合天翼云命名规则）\n\tcertName := fmt.Sprintf(\"cm%d\", time.Now().Unix())\n\n\t// 上传证书\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=152&api=17243&data=204&isNormal=1&vid=283\n\tuploadCertificateReq := &ctyuncms.UploadCertificateRequest{\n\t\tName:               lo.ToPtr(certName),\n\t\tCertificate:        lo.ToPtr(serverCertPEM),\n\t\tCertificateChain:   lo.ToPtr(intermediaCertPEM),\n\t\tPrivateKey:         lo.ToPtr(privkeyPEM),\n\t\tEncryptionStandard: lo.ToPtr(\"INTERNATIONAL\"),\n\t}\n\tuploadCertificateResp, err := c.sdkClient.UploadCertificateWithContext(ctx, uploadCertificateReq)\n\tc.logger.Debug(\"sdk request 'cms.UploadCertificate'\", slog.Any(\"request\", uploadCertificateReq), slog.Any(\"response\", uploadCertificateResp))\n\tif err != nil {\n\t\tif uploadCertificateResp != nil && uploadCertificateResp.GetError() == \"CCMS_100000067\" {\n\t\t\tif upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t} else if !upok {\n\t\t\t\treturn nil, errors.New(\"ctyun cms: no certificate found\")\n\t\t\t} else {\n\t\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\t\treturn upres, nil\n\t\t\t}\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cms.UploadCertificate': %w\", err)\n\t}\n\n\t// 获取刚刚上传证书 ID\n\tif upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM); err != nil {\n\t\treturn nil, err\n\t} else if !upok {\n\t\treturn nil, fmt.Errorf(\"could not find ssl certificate, may be upload failed\")\n\t} else {\n\t\treturn upres, nil\n\t}\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc (c *Certmgr) tryGetResultIfCertExists(ctx context.Context, certPEM string) (*certmgr.UploadResult, bool, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\t// 查询用户证书列表\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=152&api=17233&data=204&isNormal=1&vid=283\n\tgetCertificateListPageNum := 1\n\tgetCertificateListPageSize := 10\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, false, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tgetCertificateListReq := &ctyuncms.GetCertificateListRequest{\n\t\t\tPageNum:  lo.ToPtr(int32(getCertificateListPageNum)),\n\t\t\tPageSize: lo.ToPtr(int32(getCertificateListPageSize)),\n\t\t\tKeyword:  lo.ToPtr(certX509.Subject.CommonName),\n\t\t\tOrigin:   lo.ToPtr(\"UPLOAD\"),\n\t\t}\n\t\tgetCertificateListResp, err := c.sdkClient.GetCertificateListWithContext(ctx, getCertificateListReq)\n\t\tc.logger.Debug(\"sdk request 'cms.GetCertificateList'\", slog.Any(\"request\", getCertificateListReq), slog.Any(\"response\", getCertificateListResp))\n\t\tif err != nil {\n\t\t\treturn nil, false, fmt.Errorf(\"failed to execute sdk request 'cms.GetCertificateList': %w\", err)\n\t\t}\n\n\t\tif getCertificateListResp.ReturnObj == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, certItem := range getCertificateListResp.ReturnObj.List {\n\t\t\t// 对比证书名称\n\t\t\tif !strings.EqualFold(strings.Join(certX509.DNSNames, \",\"), certItem.DomainName) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书有效期\n\t\t\toldCertNotBefore, _ := time.Parse(\"2006-01-02T15:04:05Z\", certItem.IssueTime)\n\t\t\toldCertNotAfter, _ := time.Parse(\"2006-01-02T15:04:05Z\", certItem.ExpireTime)\n\t\t\tif !certX509.NotBefore.Equal(oldCertNotBefore) {\n\t\t\t\tcontinue\n\t\t\t} else if !certX509.NotAfter.Equal(oldCertNotAfter) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书指纹\n\t\t\tfingerprint := sha1.Sum(certX509.Raw)\n\t\t\tfingerprintHex := hex.EncodeToString(fingerprint[:])\n\t\t\tif !strings.EqualFold(fingerprintHex, certItem.Fingerprint) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   certItem.Id,\n\t\t\t\tCertName: certItem.Name,\n\t\t\t}, true, nil\n\t\t}\n\n\t\tif len(getCertificateListResp.ReturnObj.List) < getCertificateListPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tgetCertificateListPageNum++\n\t}\n\n\treturn nil, false, nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey string) (*ctyuncms.Client, error) {\n\treturn ctyuncms.NewClient(accessKeyId, secretAccessKey)\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ctcccloud-cms/ctcccloud_cms_test.go",
    "content": "package ctcccloudcms_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-cms\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n)\n\nfunc init() {\n\targsPrefix := \"CTCCCLOUDCMS_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ctcccloud_cms_test.go -args \\\n\t--CTCCCLOUDCMS_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--CTCCCLOUDCMS_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--CTCCCLOUDCMS_ACCESSKEYID=\"your-access-key-id\" \\\n\t--CTCCCLOUDCMS_SECRETACCESSKEY=\"your-secret-access-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ctcccloud-elb/ctcccloud_elb.go",
    "content": "package ctcccloudelb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/pocketbase/pocketbase/tools/security\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tctyunelb \"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/elb\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 天翼云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 天翼云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 天翼云资源池 ID。\n\tRegionId string `json:\"regionId\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *ctyunelb.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 查询证书列表，避免重复上传\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5692&data=88&isNormal=1&vid=82\n\tlistCertificatesReq := &ctyunelb.ListCertificatesRequest{\n\t\tRegionID: lo.ToPtr(c.config.RegionId),\n\t}\n\tlistCertificatesResp, err := c.sdkClient.ListCertificatesWithContext(ctx, listCertificatesReq)\n\tc.logger.Debug(\"sdk request 'elb.ListCertificates'\", slog.Any(\"request\", listCertificatesReq), slog.Any(\"response\", listCertificatesResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'elb.ListCertificates': %w\", err)\n\t} else {\n\t\tfor _, certItem := range listCertificatesResp.ReturnObj {\n\t\t\t// 如果已存在相同证书，直接返回\n\t\t\tif xcert.EqualCertificatesFromPEM(certPEM, certItem.Certificate) {\n\t\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\t\treturn &certmgr.UploadResult{\n\t\t\t\t\tCertId:   certItem.ID,\n\t\t\t\t\tCertName: certItem.Name,\n\t\t\t\t}, nil\n\t\t\t}\n\t\t}\n\t}\n\n\t// 生成新证书名（需符合天翼云命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 创建证书\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5685&data=88&isNormal=1&vid=82\n\tcreateCertificateReq := &ctyunelb.CreateCertificateRequest{\n\t\tClientToken: lo.ToPtr(security.RandomString(32)),\n\t\tRegionID:    lo.ToPtr(c.config.RegionId),\n\t\tName:        lo.ToPtr(certName),\n\t\tDescription: lo.ToPtr(\"upload from certimate\"),\n\t\tType:        lo.ToPtr(\"Server\"),\n\t\tCertificate: lo.ToPtr(certPEM),\n\t\tPrivateKey:  lo.ToPtr(privkeyPEM),\n\t}\n\tcreateCertificateResp, err := c.sdkClient.CreateCertificateWithContext(ctx, createCertificateReq)\n\tc.logger.Debug(\"sdk request 'elb.CreateCertificate'\", slog.Any(\"request\", createCertificateReq), slog.Any(\"response\", createCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'elb.CreateCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   createCertificateResp.ReturnObj.ID,\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey string) (*ctyunelb.Client, error) {\n\treturn ctyunelb.NewClient(accessKeyId, secretAccessKey)\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ctcccloud-elb/ctcccloud_elb_test.go",
    "content": "package ctcccloudelb_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-elb\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n\tfRegionId        string\n)\n\nfunc init() {\n\targsPrefix := \"CTCCCLOUDELB_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fRegionId, argsPrefix+\"REGIONID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ctcccloud_elb_test.go -args \\\n\t--CTCCCLOUDELB_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--CTCCCLOUDELB_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--CTCCCLOUDELB_ACCESSKEYID=\"your-access-key-id\" \\\n\t--CTCCCLOUDELB_SECRETACCESSKEY=\"your-secret-access-key\" \\\n\t--CTCCCLOUDELB_REGIONID=\"your-region-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"REGIONID: %v\", fRegionId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t\tRegionId:        fRegionId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ctcccloud-icdn/ctcccloud_icdn.go",
    "content": "package ctcccloudicdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tctyunicdn \"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/icdn\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 天翼云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 天翼云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *ctyunicdn.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 查询证书列表，避免重复上传\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10838&data=173&isNormal=1&vid=166\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10837&data=173&isNormal=1&vid=166\n\tqueryCertListPage := 1\n\tqueryCertListPerPage := 1000\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tqueryCertListReq := &ctyunicdn.QueryCertListRequest{\n\t\t\tPage:      lo.ToPtr(int32(queryCertListPage)),\n\t\t\tPerPage:   lo.ToPtr(int32(queryCertListPerPage)),\n\t\t\tUsageMode: lo.ToPtr(int32(0)),\n\t\t}\n\t\tqueryCertListResp, err := c.sdkClient.QueryCertListWithContext(ctx, queryCertListReq)\n\t\tc.logger.Debug(\"sdk request 'icdn.QueryCertList'\", slog.Any(\"request\", queryCertListReq), slog.Any(\"response\", queryCertListResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'icdn.QueryCertList': %w\", err)\n\t\t}\n\n\t\tif queryCertListResp.ReturnObj == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, certItem := range queryCertListResp.ReturnObj.Results {\n\t\t\t// 对比证书通用名称\n\t\t\tif !strings.EqualFold(certX509.Subject.CommonName, certItem.CN) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书扩展名称\n\t\t\tif !slices.Equal(certX509.DNSNames, certItem.SANs) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书有效期\n\t\t\tif !certX509.NotBefore.Equal(time.Unix(certItem.IssueTime, 0).UTC()) {\n\t\t\t\tcontinue\n\t\t\t} else if !certX509.NotAfter.Equal(time.Unix(certItem.ExpiresTime, 0).UTC()) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书内容\n\t\t\tqueryCertDetailReq := &ctyunicdn.QueryCertDetailRequest{\n\t\t\t\tId: lo.ToPtr(certItem.Id),\n\t\t\t}\n\t\t\tqueryCertDetailResp, err := c.sdkClient.QueryCertDetailWithContext(ctx, queryCertDetailReq)\n\t\t\tc.logger.Debug(\"sdk request 'icdn.QueryCertDetail'\", slog.Any(\"request\", queryCertDetailReq), slog.Any(\"response\", queryCertDetailResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'icdn.QueryCertDetail': %w\", err)\n\t\t\t} else if queryCertDetailResp.ReturnObj != nil && queryCertDetailResp.ReturnObj.Result != nil {\n\t\t\t\tif !xcert.EqualCertificatesFromPEM(certPEM, queryCertDetailResp.ReturnObj.Result.Certs) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   fmt.Sprintf(\"%d\", queryCertDetailResp.ReturnObj.Result.Id),\n\t\t\t\tCertName: queryCertDetailResp.ReturnObj.Result.Name,\n\t\t\t}, nil\n\t\t}\n\n\t\tif len(queryCertListResp.ReturnObj.Results) < queryCertListPerPage {\n\t\t\tbreak\n\t\t}\n\n\t\tqueryCertListPage++\n\t}\n\n\t// 生成新证书名（需符合天翼云命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 创建证书\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10835&data=173&isNormal=1&vid=166\n\tcreateCertReq := &ctyunicdn.CreateCertRequest{\n\t\tName:  lo.ToPtr(certName),\n\t\tCerts: lo.ToPtr(certPEM),\n\t\tKey:   lo.ToPtr(privkeyPEM),\n\t}\n\tcreateCertResp, err := c.sdkClient.CreateCertWithContext(ctx, createCertReq)\n\tc.logger.Debug(\"sdk request 'icdn.CreateCert'\", slog.Any(\"request\", createCertReq), slog.Any(\"response\", createCertResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'icdn.CreateCert': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   fmt.Sprintf(\"%d\", createCertResp.ReturnObj.Id),\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey string) (*ctyunicdn.Client, error) {\n\treturn ctyunicdn.NewClient(accessKeyId, secretAccessKey)\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ctcccloud-icdn/ctcccloud_icdn_test.go",
    "content": "package ctcccloudicdn_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-icdn\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n)\n\nfunc init() {\n\targsPrefix := \"CTCCCLOUDICDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ctcccloud_icdn_test.go -args \\\n\t--CTCCCLOUDICDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--CTCCCLOUDICDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--CTCCCLOUDICDN_ACCESSKEYID=\"your-access-key-id\" \\\n\t--CTCCCLOUDICDN_SECRETACCESSKEY=\"your-secret-access-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ctcccloud-lvdn/ctcccloud_lvdn.go",
    "content": "package ctcccloudlvdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tctyunlvdn \"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/lvdn\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 天翼云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 天翼云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *ctyunlvdn.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 查询证书列表，避免重复上传\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11452&data=183&isNormal=1&vid=261\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11449&data=183&isNormal=1&vid=261\n\tqueryCertListPage := 1\n\tqueryCertListPerPage := 1000\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tqueryCertListReq := &ctyunlvdn.QueryCertListRequest{\n\t\t\tPage:      lo.ToPtr(int32(queryCertListPage)),\n\t\t\tPerPage:   lo.ToPtr(int32(queryCertListPerPage)),\n\t\t\tUsageMode: lo.ToPtr(int32(0)),\n\t\t}\n\t\tqueryCertListResp, err := c.sdkClient.QueryCertListWithContext(ctx, queryCertListReq)\n\t\tc.logger.Debug(\"sdk request 'lvdn.QueryCertList'\", slog.Any(\"request\", queryCertListReq), slog.Any(\"response\", queryCertListResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'lvdn.QueryCertList': %w\", err)\n\t\t}\n\n\t\tif queryCertListResp.ReturnObj == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, certItem := range queryCertListResp.ReturnObj.Results {\n\t\t\t// 对比证书通用名称\n\t\t\tif !strings.EqualFold(certX509.Subject.CommonName, certItem.CN) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书扩展名称\n\t\t\tif !slices.Equal(certX509.DNSNames, certItem.SANs) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书有效期\n\t\t\tif !certX509.NotBefore.Equal(time.Unix(certItem.IssueTime, 0).UTC()) {\n\t\t\t\tcontinue\n\t\t\t} else if !certX509.NotAfter.Equal(time.Unix(certItem.ExpiresTime, 0).UTC()) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书内容\n\t\t\tqueryCertDetailReq := &ctyunlvdn.QueryCertDetailRequest{\n\t\t\t\tId: lo.ToPtr(certItem.Id),\n\t\t\t}\n\t\t\tqueryCertDetailResp, err := c.sdkClient.QueryCertDetailWithContext(ctx, queryCertDetailReq)\n\t\t\tc.logger.Debug(\"sdk request 'lvdn.QueryCertDetail'\", slog.Any(\"request\", queryCertDetailReq), slog.Any(\"response\", queryCertDetailResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'lvdn.QueryCertDetail': %w\", err)\n\t\t\t} else if queryCertDetailResp.ReturnObj != nil && queryCertDetailResp.ReturnObj.Result != nil {\n\t\t\t\tif !xcert.EqualCertificatesFromPEM(certPEM, queryCertDetailResp.ReturnObj.Result.Certs) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   fmt.Sprintf(\"%d\", queryCertDetailResp.ReturnObj.Result.Id),\n\t\t\t\tCertName: queryCertDetailResp.ReturnObj.Result.Name,\n\t\t\t}, nil\n\t\t}\n\n\t\tif len(queryCertListResp.ReturnObj.Results) < queryCertListPerPage {\n\t\t\tbreak\n\t\t}\n\n\t\tqueryCertListPage++\n\t}\n\n\t// 生成新证书名（需符合天翼云命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 创建证书\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11436&data=183&isNormal=1&vid=261\n\tcreateCertReq := &ctyunlvdn.CreateCertRequest{\n\t\tName:  lo.ToPtr(certName),\n\t\tCerts: lo.ToPtr(certPEM),\n\t\tKey:   lo.ToPtr(privkeyPEM),\n\t}\n\tcreateCertResp, err := c.sdkClient.CreateCertWithContext(ctx, createCertReq)\n\tc.logger.Debug(\"sdk request 'lvdn.CreateCert'\", slog.Any(\"request\", createCertReq), slog.Any(\"response\", createCertResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'lvdn.CreateCert': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   fmt.Sprintf(\"%d\", createCertResp.ReturnObj.Id),\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey string) (*ctyunlvdn.Client, error) {\n\treturn ctyunlvdn.NewClient(accessKeyId, secretAccessKey)\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go",
    "content": "package ctcccloudlvdn_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-lvdn\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n)\n\nfunc init() {\n\targsPrefix := \"CTCCCLOUDLVDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ctcccloud_lvdn_test.go -args \\\n\t--CTCCCLOUDLVDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--CTCCCLOUDLVDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--CTCCCLOUDLVDN_ACCESSKEYID=\"your-access-key-id\" \\\n\t--CTCCCLOUDLVDN_SECRETACCESSKEY=\"your-secret-access-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/dogecloud/dogecloud.go",
    "content": "package dogecloud\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tdogesdk \"github.com/certimate-go/certimate/pkg/sdk3rd/dogecloud\"\n)\n\ntype CertmgrConfig struct {\n\t// 多吉云 AccessKey。\n\tAccessKey string `json:\"accessKey\"`\n\t// 多吉云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *dogesdk.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKey, config.SecretKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 生成新证书名（需符合多吉云命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 上传新证书\n\t// REF: https://docs.dogecloud.com/cdn/api-cert-upload\n\tuploadSslCertReq := &dogesdk.UploadCdnCertRequest{\n\t\tNote:        certName,\n\t\tCertificate: certPEM,\n\t\tPrivateKey:  privkeyPEM,\n\t}\n\tuploadSslCertResp, err := c.sdkClient.UploadCdnCertWithContext(ctx, uploadSslCertReq)\n\tc.logger.Debug(\"sdk request 'cdn.UploadCdnCert'\", slog.Any(\"request\", uploadSslCertReq), slog.Any(\"response\", uploadSslCertResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.UploadCdnCert': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   fmt.Sprintf(\"%d\", uploadSslCertResp.Data.Id),\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(accessKey, secretKey string) (*dogesdk.Client, error) {\n\treturn dogesdk.NewClient(accessKey, secretKey)\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/dokploy/dokploy.go",
    "content": "package dokploy\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tdokploysdk \"github.com/certimate-go/certimate/pkg/sdk3rd/dokploy\"\n)\n\ntype CertmgrConfig struct {\n\t// Dokploy 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// Dokploy API Key。\n\tApiKey string `json:\"apiKey\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *dokploysdk.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 查询证书列表，避免重复上传\n\t// REF: https://docs.dokploy.com/docs/api/certificates#certificates-all\n\tcertificatesAllReq := &dokploysdk.CertificatesAllRequest{}\n\tcertificatesAllResp, err := c.sdkClient.CertificatesAllWithContext(ctx, certificatesAllReq)\n\tc.logger.Debug(\"sdk request 'certificates.all'\", slog.Any(\"request\", certificatesAllReq), slog.Any(\"response\", certificatesAllResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'certificates.all': %w\", err)\n\t} else {\n\t\tfor _, certItem := range *certificatesAllResp {\n\t\t\tif certItem.CertificateData == certPEM && certItem.PrivateKey == privkeyPEM {\n\t\t\t\t// 如果已存在相同证书，直接返回\n\t\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\t\treturn &certmgr.UploadResult{\n\t\t\t\t\tCertId:   certItem.CertificateId,\n\t\t\t\t\tCertName: certItem.Name,\n\t\t\t\t}, nil\n\t\t\t}\n\t\t}\n\t}\n\n\t// 获取账号信息，找到默认的组织 ID\n\t// REF: https://docs.dokploy.com/docs/api/reference-user#user.get\n\tuserGetReq := &dokploysdk.UserGetRequest{}\n\tuserGetResp, err := c.sdkClient.UserGetWithContext(ctx, userGetReq)\n\tc.logger.Debug(\"sdk request 'user.get'\", slog.Any(\"request\", userGetReq), slog.Any(\"response\", userGetResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'user.get': %w\", err)\n\t}\n\n\t// 创建证书\n\t// REF: https://docs.dokploy.com/docs/api/certificates#certificates-create\n\tcertificatesCreateReq := &dokploysdk.CertificatesCreateRequest{\n\t\tName:            lo.ToPtr(fmt.Sprintf(\"certimate-%d\", time.Now().Unix())),\n\t\tCertificateData: lo.ToPtr(certPEM),\n\t\tPrivateKey:      lo.ToPtr(privkeyPEM),\n\t\tOrganizationId:  lo.ToPtr(userGetResp.OrganizationId),\n\t}\n\tcertificatesCreateResp, err := c.sdkClient.CertificatesCreateWithContext(ctx, certificatesCreateReq)\n\tc.logger.Debug(\"sdk request 'certificates.create'\", slog.Any(\"request\", certificatesCreateReq), slog.Any(\"response\", certificatesCreateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'certificates.create': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   certificatesCreateResp.CertificateId,\n\t\tCertName: certificatesCreateResp.Name,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*dokploysdk.Client, error) {\n\tclient, err := dokploysdk.NewClient(serverUrl, apiKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/dokploy/dokploy_test.go",
    "content": "package dokploy_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/dokploy\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfApiKey        string\n)\n\nfunc init() {\n\targsPrefix := \"DOKPLOY_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./dokploy_test.go -args \\\n\t--DOKPLOY_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--DOKPLOY_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--DOKPLOY_SERVERURL=\"http://127.0.0.1:3000\" \\\n\t--DOKPLOY_APIKEY=\"your-api-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiKey:                   fApiKey,\n\t\t\tAllowInsecureConnections: true,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/gcore-cdn/gcore_cdn.go",
    "content": "package gcorecdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\tgcore \"github.com/G-Core/gcorelabscdn-go/gcore/provider\"\n\t\"github.com/G-Core/gcorelabscdn-go/sslcerts\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tgcoresdk \"github.com/certimate-go/certimate/pkg/sdk3rd/gcore\"\n)\n\ntype CertmgrConfig struct {\n\t// G-Core API Token。\n\tApiToken string `json:\"apiToken\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *sslcerts.Service\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ApiToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 新增证书\n\t// REF: https://api.gcore.com/docs/cdn#tag/SSL-certificates/operation/add_ssl_certificates\n\tcreateCertificateReq := &sslcerts.CreateRequest{\n\t\tName:           fmt.Sprintf(\"certimate_%d\", time.Now().UnixMilli()),\n\t\tCert:           certPEM,\n\t\tPrivateKey:     privkeyPEM,\n\t\tAutomated:      false,\n\t\tValidateRootCA: false,\n\t}\n\tcreateCertificateResp, err := c.sdkClient.Create(ctx, createCertificateReq)\n\tc.logger.Debug(\"sdk request 'sslcerts.Create'\", slog.Any(\"request\", createCertificateReq), slog.Any(\"response\", createCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'sslcerts.Create': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   fmt.Sprintf(\"%d\", createCertificateResp.ID),\n\t\tCertName: createCertificateResp.Name,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(apiToken string) (*sslcerts.Service, error) {\n\tif apiToken == \"\" {\n\t\treturn nil, errors.New(\"gcore: invalid api token\")\n\t}\n\n\trequester := gcore.NewClient(\n\t\tgcoresdk.BASE_URL,\n\t\tgcore.WithSigner(gcoresdk.NewAuthRequestSigner(apiToken)),\n\t)\n\tservice := sslcerts.NewService(requester)\n\treturn service, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/huaweicloud-elb/huaweicloud_elb.go",
    "content": "package huaweicloudelb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic\"\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global\"\n\thcelb \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3\"\n\thcelbmodel \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model\"\n\thcelbregion \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/region\"\n\thciam \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3\"\n\thciammodel \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model\"\n\thciamregion \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr/providers/huaweicloud-elb/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 华为云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 华为云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 华为云企业项目 ID。\n\tEnterpriseProjectId string `json:\"enterpriseProjectId,omitempty\"`\n\t// 华为云区域。\n\tRegion string `json:\"region\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *internal.ElbClient\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 查询已有证书，避免重复上传\n\t// REF: https://support.huaweicloud.com/api-elb/ListCertificates.html\n\tlistCertificatesMarker := (*string)(nil)\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistCertificatesReq := &hcelbmodel.ListCertificatesRequest{\n\t\t\tMarker: listCertificatesMarker,\n\t\t\tLimit:  lo.ToPtr(int32(2000)),\n\t\t\tType:   lo.ToPtr([]string{\"server\"}),\n\t\t}\n\t\tlistCertificatesResp, err := c.sdkClient.ListCertificates(listCertificatesReq)\n\t\tc.logger.Debug(\"sdk request 'elb.ListCertificates'\", slog.Any(\"request\", listCertificatesReq), slog.Any(\"response\", listCertificatesResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'elb.ListCertificates': %w\", err)\n\t\t}\n\n\t\tif listCertificatesResp.Certificates == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, certItem := range *listCertificatesResp.Certificates {\n\t\t\t// 如果已存在相同证书，直接返回\n\t\t\tif xcert.EqualCertificatesFromPEM(certPEM, certItem.Certificate) {\n\t\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\t\treturn &certmgr.UploadResult{\n\t\t\t\t\tCertId:   certItem.Id,\n\t\t\t\t\tCertName: certItem.Name,\n\t\t\t\t}, nil\n\t\t\t}\n\t\t}\n\n\t\tif len(*listCertificatesResp.Certificates) == 0 || listCertificatesResp.PageInfo.NextMarker == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tlistCertificatesMarker = listCertificatesResp.PageInfo.NextMarker\n\t}\n\n\t// 获取项目 ID\n\t// REF: https://support.huaweicloud.com/api-iam/iam_06_0001.html\n\tprojectId, err := getSDKProjectId(c.config.AccessKeyId, c.config.SecretAccessKey, c.config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get SDK project id: %w\", err)\n\t}\n\n\t// 生成新证书名（需符合华为云命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 创建新证书\n\t// REF: https://support.huaweicloud.com/api-elb/CreateCertificate.html\n\tcreateCertificateReq := &hcelbmodel.CreateCertificateRequest{\n\t\tBody: &hcelbmodel.CreateCertificateRequestBody{\n\t\t\tCertificate: &hcelbmodel.CreateCertificateOption{\n\t\t\t\tEnterpriseProjectId: lo.EmptyableToPtr(c.config.EnterpriseProjectId),\n\t\t\t\tProjectId:           lo.ToPtr(projectId),\n\t\t\t\tName:                lo.ToPtr(certName),\n\t\t\t\tCertificate:         lo.ToPtr(certPEM),\n\t\t\t\tPrivateKey:          lo.ToPtr(privkeyPEM),\n\t\t\t},\n\t\t},\n\t}\n\tcreateCertificateResp, err := c.sdkClient.CreateCertificate(createCertificateReq)\n\tc.logger.Debug(\"sdk request 'elb.CreateCertificate'\", slog.Any(\"request\", createCertificateReq), slog.Any(\"response\", createCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'elb.CreateCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   createCertificateResp.Certificate.Id,\n\t\tCertName: createCertificateResp.Certificate.Name,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\t// 更新证书\n\t// REF: https://support.huaweicloud.com/api-elb/UpdateCertificate.html\n\tupdateCertificateReq := &hcelbmodel.UpdateCertificateRequest{\n\t\tCertificateId: certIdOrName,\n\t\tBody: &hcelbmodel.UpdateCertificateRequestBody{\n\t\t\tCertificate: &hcelbmodel.UpdateCertificateOption{\n\t\t\t\tCertificate: lo.ToPtr(certPEM),\n\t\t\t\tPrivateKey:  lo.ToPtr(privkeyPEM),\n\t\t\t},\n\t\t},\n\t}\n\tupdateCertificateResp, err := c.sdkClient.UpdateCertificate(updateCertificateReq)\n\tc.logger.Debug(\"sdk request 'elb.UpdateCertificate'\", slog.Any(\"request\", updateCertificateReq), slog.Any(\"response\", updateCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'elb.UpdateCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.OperateResult{}, nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey, region string) (*internal.ElbClient, error) {\n\tif region == \"\" {\n\t\tregion = \"cn-north-4\" // ELB 服务默认区域：华北四北京\n\t}\n\n\tauth, err := basic.NewCredentialsBuilder().\n\t\tWithAk(accessKeyId).\n\t\tWithSk(secretAccessKey).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thcRegion, err := hcelbregion.SafeValueOf(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thcClient, err := hcelb.ElbClientBuilder().\n\t\tWithRegion(hcRegion).\n\t\tWithCredential(auth).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := internal.NewElbClient(hcClient)\n\treturn client, nil\n}\n\nfunc getSDKProjectId(accessKeyId, secretAccessKey, region string) (string, error) {\n\tif region == \"\" {\n\t\tregion = \"cn-north-4\" // IAM 服务默认区域：华北四北京\n\t}\n\n\tauth, err := global.NewCredentialsBuilder().\n\t\tWithAk(accessKeyId).\n\t\tWithSk(secretAccessKey).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thcRegion, err := hciamregion.SafeValueOf(region)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thcClient, err := hciam.IamClientBuilder().\n\t\tWithRegion(hcRegion).\n\t\tWithCredential(auth).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tclient := hciam.NewIamClient(hcClient)\n\n\trequest := &hciammodel.KeystoneListProjectsRequest{\n\t\tName: &region,\n\t}\n\tresponse, err := client.KeystoneListProjects(request)\n\tif err != nil {\n\t\treturn \"\", err\n\t} else if response.Projects == nil || len(*response.Projects) == 0 {\n\t\treturn \"\", errors.New(\"huaweicloud: no project found\")\n\t}\n\n\treturn (*response.Projects)[0].Id, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/huaweicloud-elb/internal/client.go",
    "content": "package internal\n\nimport (\n\thttpclient \"github.com/huaweicloud/huaweicloud-sdk-go-v3/core\"\n\thwelb \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3\"\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model\"\n)\n\n// This is a partial copy of https://github.com/huaweicloud/huaweicloud-sdk-go-v3/blob/master/services/elb/v3/elb_client.go\n// to lightweight the vendor packages in the built binary.\ntype ElbClient struct {\n\tHcClient *httpclient.HcHttpClient\n}\n\nfunc NewElbClient(hcClient *httpclient.HcHttpClient) *ElbClient {\n\treturn &ElbClient{HcClient: hcClient}\n}\n\nfunc (c *ElbClient) CreateCertificate(request *model.CreateCertificateRequest) (*model.CreateCertificateResponse, error) {\n\trequestDef := hwelb.GenReqDefForCreateCertificate()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.CreateCertificateResponse), nil\n\t}\n}\n\nfunc (c *ElbClient) ListCertificates(request *model.ListCertificatesRequest) (*model.ListCertificatesResponse, error) {\n\trequestDef := hwelb.GenReqDefForListCertificates()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ListCertificatesResponse), nil\n\t}\n}\n\nfunc (c *ElbClient) UpdateCertificate(request *model.UpdateCertificateRequest) (*model.UpdateCertificateResponse, error) {\n\trequestDef := hwelb.GenReqDefForUpdateCertificate()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.UpdateCertificateResponse), nil\n\t}\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/huaweicloud-scm/huaweicloud_scm.go",
    "content": "package huaweicloudscm\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic\"\n\thcscm \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3\"\n\thcscmmodel \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3/model\"\n\thcscmregion \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3/region\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr/providers/huaweicloud-scm/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 华为云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 华为云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 华为云企业项目 ID。\n\tEnterpriseProjectId string `json:\"enterpriseProjectId,omitempty\"`\n\t// 华为云区域。\n\tRegion string `json:\"region\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *internal.ScmClient\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 查询已有证书，避免重复上传\n\t// REF: https://support.huaweicloud.com/api-ccm/ListCertificates.html\n\t// REF: https://support.huaweicloud.com/api-ccm/ExportCertificate_0.html\n\tlistCertificatesLimit := 50\n\tlistCertificatesOffset := 0\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistCertificatesReq := &hcscmmodel.ListCertificatesRequest{\n\t\t\tEnterpriseProjectId: lo.EmptyableToPtr(c.config.EnterpriseProjectId),\n\t\t\tLimit:               lo.ToPtr(int32(listCertificatesLimit)),\n\t\t\tOffset:              lo.ToPtr(int32(listCertificatesOffset)),\n\t\t\tSortDir:             lo.ToPtr(\"DESC\"),\n\t\t\tSortKey:             lo.ToPtr(\"certExpiredTime\"),\n\t\t}\n\t\tlistCertificatesResp, err := c.sdkClient.ListCertificates(listCertificatesReq)\n\t\tc.logger.Debug(\"sdk request 'scm.ListCertificates'\", slog.Any(\"request\", listCertificatesReq), slog.Any(\"response\", listCertificatesResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'scm.ListCertificates': %w\", err)\n\t\t}\n\n\t\tif listCertificatesResp.Certificates == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, certItem := range *listCertificatesResp.Certificates {\n\t\t\t// 对比证书通用名称\n\t\t\tif !strings.EqualFold(certX509.Subject.CommonName, certItem.Domain) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书有效期\n\t\t\tif certX509.NotAfter.Local().Format(time.DateTime) != strings.TrimSuffix(certItem.ExpireTime, \".0\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书内容\n\t\t\texportCertificateReq := &hcscmmodel.ExportCertificateRequest{\n\t\t\t\tCertificateId: certItem.Id,\n\t\t\t}\n\t\t\texportCertificateResp, err := c.sdkClient.ExportCertificate(exportCertificateReq)\n\t\t\tc.logger.Debug(\"sdk request 'scm.ExportCertificate'\", slog.Any(\"request\", exportCertificateReq), slog.Any(\"response\", exportCertificateResp))\n\t\t\tif err != nil {\n\t\t\t\tif exportCertificateResp != nil && exportCertificateResp.HttpStatusCode == 404 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'scm.ExportCertificate': %w\", err)\n\t\t\t} else {\n\t\t\t\tif !xcert.EqualCertificatesFromPEM(certPEM, lo.FromPtr(exportCertificateResp.Certificate)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   certItem.Id,\n\t\t\t\tCertName: certItem.Name,\n\t\t\t}, nil\n\t\t}\n\n\t\tif len(*listCertificatesResp.Certificates) < listCertificatesLimit {\n\t\t\tbreak\n\t\t}\n\n\t\tlistCertificatesOffset += listCertificatesLimit\n\t}\n\n\t// 生成新证书名（需符合华为云命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 上传新证书\n\t// REF: https://support.huaweicloud.com/api-ccm/ImportCertificate.html\n\timportCertificateReq := &hcscmmodel.ImportCertificateRequest{\n\t\tBody: &hcscmmodel.ImportCertificateRequestBody{\n\t\t\tEnterpriseProjectId: lo.EmptyableToPtr(c.config.EnterpriseProjectId),\n\t\t\tName:                certName,\n\t\t\tCertificate:         certPEM,\n\t\t\tPrivateKey:          privkeyPEM,\n\t\t},\n\t}\n\timportCertificateResp, err := c.sdkClient.ImportCertificate(importCertificateReq)\n\tc.logger.Debug(\"sdk request 'scm.ImportCertificate'\", slog.Any(\"request\", importCertificateReq), slog.Any(\"response\", importCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'scm.ImportCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   *importCertificateResp.CertificateId,\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey, region string) (*internal.ScmClient, error) {\n\tif region == \"\" {\n\t\tregion = \"cn-north-4\" // SCM 服务默认区域：华北四北京\n\t}\n\n\tauth, err := basic.NewCredentialsBuilder().\n\t\tWithAk(accessKeyId).\n\t\tWithSk(secretAccessKey).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thcRegion, err := hcscmregion.SafeValueOf(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thcClient, err := hcscm.ScmClientBuilder().\n\t\tWithRegion(hcRegion).\n\t\tWithCredential(auth).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := internal.NewScmClient(hcClient)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/huaweicloud-scm/internal/client.go",
    "content": "package internal\n\nimport (\n\thttpclient \"github.com/huaweicloud/huaweicloud-sdk-go-v3/core\"\n\thwscm \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3\"\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3/model\"\n)\n\n// This is a partial copy of https://github.com/huaweicloud/huaweicloud-sdk-go-v3/blob/master/services/scm/v3/scm_client.go\n// to lightweight the vendor packages in the built binary.\ntype ScmClient struct {\n\tHcClient *httpclient.HcHttpClient\n}\n\nfunc NewScmClient(hcClient *httpclient.HcHttpClient) *ScmClient {\n\treturn &ScmClient{HcClient: hcClient}\n}\n\nfunc (c *ScmClient) ExportCertificate(request *model.ExportCertificateRequest) (*model.ExportCertificateResponse, error) {\n\trequestDef := hwscm.GenReqDefForExportCertificate()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ExportCertificateResponse), nil\n\t}\n}\n\nfunc (c *ScmClient) ImportCertificate(request *model.ImportCertificateRequest) (*model.ImportCertificateResponse, error) {\n\trequestDef := hwscm.GenReqDefForImportCertificate()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ImportCertificateResponse), nil\n\t}\n}\n\nfunc (c *ScmClient) ListCertificates(request *model.ListCertificatesRequest) (*model.ListCertificatesResponse, error) {\n\trequestDef := hwscm.GenReqDefForListCertificates()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ListCertificatesResponse), nil\n\t}\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/huaweicloud-waf/huaweicloud_waf.go",
    "content": "package huaweicloudwaf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic\"\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global\"\n\thciam \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3\"\n\thciammodel \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model\"\n\thciamregion \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region\"\n\thcwaf \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1\"\n\thcwafmodel \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/model\"\n\thcwafregion \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/region\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr/providers/huaweicloud-waf/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 华为云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 华为云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 华为云企业项目 ID。\n\tEnterpriseProjectId string `json:\"enterpriseProjectId,omitempty\"`\n\t// 华为云区域。\n\tRegion string `json:\"region\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *internal.WafClient\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 查询已有证书，避免重复上传\n\t// REF: https://support.huaweicloud.com/api-waf/ListCertificates.html\n\t// REF: https://support.huaweicloud.com/api-waf/ShowCertificate.html\n\tlistCertificatesPage := 1\n\tlistCertificatesPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistCertificatesReq := &hcwafmodel.ListCertificatesRequest{\n\t\t\tEnterpriseProjectId: lo.EmptyableToPtr(c.config.EnterpriseProjectId),\n\t\t\tPage:                lo.ToPtr(int32(listCertificatesPage)),\n\t\t\tPagesize:            lo.ToPtr(int32(listCertificatesPageSize)),\n\t\t}\n\t\tlistCertificatesResp, err := c.sdkClient.ListCertificates(listCertificatesReq)\n\t\tc.logger.Debug(\"sdk request 'waf.ShowCertificate'\", slog.Any(\"request\", listCertificatesReq), slog.Any(\"response\", listCertificatesResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'waf.ListCertificates': %w\", err)\n\t\t}\n\n\t\tif listCertificatesResp.Items == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, certItem := range *listCertificatesResp.Items {\n\t\t\tshowCertificateReq := &hcwafmodel.ShowCertificateRequest{\n\t\t\t\tEnterpriseProjectId: lo.EmptyableToPtr(c.config.EnterpriseProjectId),\n\t\t\t\tCertificateId:       certItem.Id,\n\t\t\t}\n\t\t\tshowCertificateResp, err := c.sdkClient.ShowCertificate(showCertificateReq)\n\t\t\tc.logger.Debug(\"sdk request 'waf.ShowCertificate'\", slog.Any(\"request\", showCertificateReq), slog.Any(\"response\", showCertificateResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'waf.ShowCertificate': %w\", err)\n\t\t\t}\n\n\t\t\t// 如果已存在相同证书，直接返回\n\t\t\tif xcert.EqualCertificatesFromPEM(certPEM, lo.FromPtr(showCertificateResp.Content)) {\n\t\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\t\treturn &certmgr.UploadResult{\n\t\t\t\t\tCertId:   certItem.Id,\n\t\t\t\t\tCertName: certItem.Name,\n\t\t\t\t}, nil\n\t\t\t}\n\t\t}\n\n\t\tif len(*listCertificatesResp.Items) < listCertificatesPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tlistCertificatesPage++\n\t}\n\n\t// 生成新证书名（需符合华为云命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 创建证书\n\t// REF: https://support.huaweicloud.com/api-waf/CreateCertificate.html\n\tcreateCertificateReq := &hcwafmodel.CreateCertificateRequest{\n\t\tEnterpriseProjectId: lo.EmptyableToPtr(c.config.EnterpriseProjectId),\n\t\tBody: &hcwafmodel.CreateCertificateRequestBody{\n\t\t\tName:    certName,\n\t\t\tContent: certPEM,\n\t\t\tKey:     privkeyPEM,\n\t\t},\n\t}\n\tcreateCertificateResp, err := c.sdkClient.CreateCertificate(createCertificateReq)\n\tc.logger.Debug(\"sdk request 'waf.CreateCertificate'\", slog.Any(\"request\", createCertificateReq), slog.Any(\"response\", createCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'waf.CreateCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   *createCertificateResp.Id,\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey, region string) (*internal.WafClient, error) {\n\tprojectId, err := getSDKProjectId(accessKeyId, secretAccessKey, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tauth, err := basic.NewCredentialsBuilder().\n\t\tWithAk(accessKeyId).\n\t\tWithSk(secretAccessKey).\n\t\tWithProjectId(projectId).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thcRegion, err := hcwafregion.SafeValueOf(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thcClient, err := hcwaf.WafClientBuilder().\n\t\tWithRegion(hcRegion).\n\t\tWithCredential(auth).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := internal.NewWafClient(hcClient)\n\treturn client, nil\n}\n\nfunc getSDKProjectId(accessKeyId, secretAccessKey, region string) (string, error) {\n\tauth, err := global.NewCredentialsBuilder().\n\t\tWithAk(accessKeyId).\n\t\tWithSk(secretAccessKey).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thcRegion, err := hciamregion.SafeValueOf(region)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thcClient, err := hciam.IamClientBuilder().\n\t\tWithRegion(hcRegion).\n\t\tWithCredential(auth).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tclient := hciam.NewIamClient(hcClient)\n\n\trequest := &hciammodel.KeystoneListProjectsRequest{\n\t\tName: &region,\n\t}\n\tresponse, err := client.KeystoneListProjects(request)\n\tif err != nil {\n\t\treturn \"\", err\n\t} else if response.Projects == nil || len(*response.Projects) == 0 {\n\t\treturn \"\", errors.New(\"huaweicloud: no project found\")\n\t}\n\n\treturn (*response.Projects)[0].Id, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/huaweicloud-waf/internal/client.go",
    "content": "package internal\n\nimport (\n\thttpclient \"github.com/huaweicloud/huaweicloud-sdk-go-v3/core\"\n\thwwaf \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1\"\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/model\"\n)\n\n// This is a partial copy of https://github.com/huaweicloud/huaweicloud-sdk-go-v3/blob/master/services/waf/v1/waf_client.go\n// to lightweight the vendor packages in the built binary.\ntype WafClient struct {\n\tHcClient *httpclient.HcHttpClient\n}\n\nfunc NewWafClient(hcClient *httpclient.HcHttpClient) *WafClient {\n\treturn &WafClient{HcClient: hcClient}\n}\n\nfunc (c *WafClient) CreateCertificate(request *model.CreateCertificateRequest) (*model.CreateCertificateResponse, error) {\n\trequestDef := hwwaf.GenReqDefForCreateCertificate()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.CreateCertificateResponse), nil\n\t}\n}\n\nfunc (c *WafClient) ListCertificates(request *model.ListCertificatesRequest) (*model.ListCertificatesResponse, error) {\n\trequestDef := hwwaf.GenReqDefForListCertificates()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ListCertificatesResponse), nil\n\t}\n}\n\nfunc (c *WafClient) ShowCertificate(request *model.ShowCertificateRequest) (*model.ShowCertificateResponse, error) {\n\trequestDef := hwwaf.GenReqDefForShowCertificate()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ShowCertificateResponse), nil\n\t}\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/jdcloud-ssl/jdcloud_ssl.go",
    "content": "package jdcloudssl\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\tjdcore \"github.com/jdcloud-api/jdcloud-sdk-go/core\"\n\tjdsslapi \"github.com/jdcloud-api/jdcloud-sdk-go/services/ssl/apis\"\n\tjdsslclient \"github.com/jdcloud-api/jdcloud-sdk-go/services/ssl/client\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 京东云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 京东云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *jdsslclient.SslClient\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 格式化私钥内容，以便后续计算私钥摘要\n\tprivkeyPEM = strings.TrimSpace(privkeyPEM)\n\tprivkeyPEM = strings.ReplaceAll(privkeyPEM, \"\\r\", \"\")\n\tprivkeyPEM = strings.ReplaceAll(privkeyPEM, \"\\n\", \"\\r\\n\")\n\tprivkeyPEM = privkeyPEM + \"\\r\\n\"\n\n\t// 查看证书列表\n\t// REF: https://docs.jdcloud.com/cn/ssl-certificate/api/describecerts\n\tdescribeCertsPageNumber := 1\n\tdescribeCertsPageSize := 10\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeCertsReq := jdsslapi.NewDescribeCertsRequestWithoutParam()\n\t\tdescribeCertsReq.SetDomainName(certX509.Subject.CommonName)\n\t\tdescribeCertsReq.SetPageNumber(describeCertsPageNumber)\n\t\tdescribeCertsReq.SetPageSize(describeCertsPageSize)\n\t\tdescribeCertsResp, err := c.sdkClient.DescribeCerts(describeCertsReq)\n\t\tc.logger.Debug(\"sdk request 'ssl.DescribeCerts'\", slog.Any(\"request\", describeCertsReq), slog.Any(\"response\", describeCertsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'ssl.DescribeCerts': %w\", err)\n\t\t}\n\n\t\tfor _, certItem := range describeCertsResp.Result.CertListDetails {\n\t\t\t// 对比证书通用名称\n\t\t\tif !strings.EqualFold(certX509.Subject.CommonName, certItem.CommonName) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书多域名\n\t\t\tif !strings.EqualFold(strings.Join(certX509.DNSNames, \",\"), strings.Join(certItem.DnsNames, \",\")) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书有效期\n\t\t\toldCertNotBefore, _ := time.Parse(time.RFC3339, certItem.StartTime)\n\t\t\toldCertNotAfter, _ := time.Parse(time.RFC3339, certItem.EndTime)\n\t\t\tif !certX509.NotBefore.Equal(oldCertNotBefore) || !certX509.NotAfter.Equal(oldCertNotAfter) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比私钥 SHA-256 摘要\n\t\t\tnewKeyDigest := sha256.Sum256([]byte(privkeyPEM))\n\t\t\tnewKeyDigestHex := hex.EncodeToString(newKeyDigest[:])\n\t\t\tif !strings.EqualFold(newKeyDigestHex, certItem.Digest) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   certItem.CertId,\n\t\t\t\tCertName: certItem.CertName,\n\t\t\t}, nil\n\t\t}\n\n\t\tif len(describeCertsResp.Result.CertListDetails) < int(describeCertsPageSize) {\n\t\t\tbreak\n\t\t} else {\n\t\t\tdescribeCertsPageNumber++\n\t\t}\n\t}\n\n\t// 生成新证书名（需符合京东云命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 上传证书\n\t// REF: https://docs.jdcloud.com/cn/ssl-certificate/api/uploadcert\n\tuploadCertReq := jdsslapi.NewUploadCertRequestWithoutParam()\n\tuploadCertReq.SetCertName(certName)\n\tuploadCertReq.SetCertFile(certPEM)\n\tuploadCertReq.SetKeyFile(privkeyPEM)\n\tuploadCertResp, err := c.sdkClient.UploadCert(uploadCertReq)\n\tc.logger.Debug(\"sdk request 'ssl.UploadCertificate'\", slog.Any(\"request\", uploadCertReq), slog.Any(\"response\", uploadCertResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'ssl.UploadCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   uploadCertResp.Result.CertId,\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret string) (*jdsslclient.SslClient, error) {\n\tclientCredentials := jdcore.NewCredentials(accessKeyId, accessKeySecret)\n\tclient := jdsslclient.NewSslClient(clientCredentials)\n\tclient.DisableLogger()\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/jdcloud-ssl/jdcloud_ssl_test.go",
    "content": "package jdcloudssl_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/jdcloud-ssl\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n)\n\nfunc init() {\n\targsPrefix := \"JDCLOUDSSL_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./jdcloud_ssl_test.go -args \\\n\t--JDCLOUDSSL_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--JDCLOUDSSL_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--JDCLOUDSSL_ACCESSKEYID=\"your-access-key-id\" \\\n\t--JDCLOUDSSL_ACCESSKEYSECRET=\"your-access-key-secret\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/nginxproxymanager/consts.go",
    "content": "package nginxproxymanager\n\nconst (\n\tAUTH_METHOD_PASSWORD = \"password\"\n\tAUTH_METHOD_TOKEN    = \"token\"\n)\n"
  },
  {
    "path": "pkg/core/certmgr/providers/nginxproxymanager/nginxproxymanager.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tnpmsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/nginxproxymanager\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// NPM 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// NPM API 认证方式。\n\t// 可取值 \"password\"、\"token\"。\n\t// 零值时默认值 [AUTH_METHOD_PASSWORD]。\n\tAuthMethod string `json:\"authMethod,omitempty\"`\n\t// NPM 用户名。\n\tUsername string `json:\"username,omitempty\"`\n\t// NPM 密码。\n\tPassword string `json:\"password,omitempty\"`\n\t// NPM API Token。\n\tApiToken string `json:\"apiToken,omitempty\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *npmsdk.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.AuthMethod, config.Username, config.Password, config.ApiToken, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 提取服务器证书和中间证书\n\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to extract certs: %w\", err)\n\t}\n\n\t// 获取全部证书，避免重复上传\n\tlistCertificatesReq := &npmsdk.NginxListCertificatesRequest{}\n\tlistCertificatesResp, err := c.sdkClient.NginxListCertificatesWithContext(ctx, listCertificatesReq)\n\tc.logger.Debug(\"sdk request 'nginx.ListCertificates'\", slog.Any(\"request\", listCertificatesReq), slog.Any(\"response\", listCertificatesResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'nginx.ListCertificates': %w\", err)\n\t} else {\n\t\tfor _, certItem := range *listCertificatesResp {\n\t\t\tif certItem.Meta.Certificate == serverCertPEM &&\n\t\t\t\tcertItem.Meta.CertificateKey == privkeyPEM &&\n\t\t\t\tcertItem.Meta.IntermediateCertificate == intermediaCertPEM {\n\t\t\t\t// 如果已存在相同证书，直接返回\n\t\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\t\treturn &certmgr.UploadResult{\n\t\t\t\t\tCertId:   fmt.Sprintf(\"%d\", certItem.Id),\n\t\t\t\t\tCertName: certItem.NiceName,\n\t\t\t\t}, nil\n\t\t\t}\n\t\t}\n\t}\n\n\t// 创建证书\n\tnginxCreateCertificateReq := &npmsdk.NginxCreateCertificateRequest{\n\t\tNiceName: fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli()),\n\t\tProvider: \"other\",\n\t}\n\tnginxCreateCertificateResp, err := c.sdkClient.NginxCreateCertificateWithContext(ctx, nginxCreateCertificateReq)\n\tc.logger.Debug(\"sdk request 'nginx.CreateCertificate'\", slog.Any(\"request\", nginxCreateCertificateReq), slog.Any(\"response\", nginxCreateCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'nginx.CreateCertificate': %w\", err)\n\t}\n\n\t// 上传证书文件\n\tngincxUploadCertificateReq := &npmsdk.NginxUploadCertificateRequest{\n\t\tCertificateMeta: npmsdk.CertificateMeta{\n\t\t\tCertificate:             serverCertPEM,\n\t\t\tCertificateKey:          privkeyPEM,\n\t\t\tIntermediateCertificate: intermediaCertPEM,\n\t\t},\n\t}\n\tngincxUploadCertificateResp, err := c.sdkClient.NginxUploadCertificateWithContext(ctx, nginxCreateCertificateResp.Id, ngincxUploadCertificateReq)\n\tc.logger.Debug(\"sdk request 'nginx.UploadCertificate'\", slog.Int64(\"request.certId\", nginxCreateCertificateResp.Id), slog.Any(\"request\", ngincxUploadCertificateReq), slog.Any(\"response\", ngincxUploadCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'nginx.UploadCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   fmt.Sprintf(\"%d\", nginxCreateCertificateResp.Id),\n\t\tCertName: nginxCreateCertificateResp.NiceName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\tcertId, err := strconv.ParseInt(certIdOrName, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 提取服务器证书和中间证书\n\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to extract certs: %w\", err)\n\t}\n\n\t// 上传证书文件\n\tngincxUploadCertificateReq := &npmsdk.NginxUploadCertificateRequest{\n\t\tCertificateMeta: npmsdk.CertificateMeta{\n\t\t\tCertificate:             serverCertPEM,\n\t\t\tCertificateKey:          privkeyPEM,\n\t\t\tIntermediateCertificate: intermediaCertPEM,\n\t\t},\n\t}\n\tngincxUploadCertificateResp, err := c.sdkClient.NginxUploadCertificateWithContext(ctx, certId, ngincxUploadCertificateReq)\n\tc.logger.Debug(\"sdk request 'nginx.UploadCertificate'\", slog.Int64(\"request.certId\", certId), slog.Any(\"request\", ngincxUploadCertificateReq), slog.Any(\"response\", ngincxUploadCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'nginx.UploadCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.OperateResult{}, nil\n}\n\nfunc createSDKClient(serverUrl, authMethod, username, password, apiToken string, skipTlsVerify bool) (*npmsdk.Client, error) {\n\tvar client *npmsdk.Client\n\tvar err error\n\n\tswitch authMethod {\n\tcase \"\", AUTH_METHOD_PASSWORD:\n\t\t{\n\t\t\tclient, err = npmsdk.NewClient(serverUrl, username, password)\n\t\t}\n\n\tcase AUTH_METHOD_TOKEN:\n\t\t{\n\t\t\tclient, err = npmsdk.NewClientWithJwtToken(serverUrl, apiToken)\n\t\t}\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/nginxproxymanager/nginxproxymanager_test.go",
    "content": "package nginxproxymanager_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/nginxproxymanager\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfUsername      string\n\tfPassword      string\n)\n\nfunc init() {\n\targsPrefix := \"NGINXPROXYMANAGER_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fUsername, argsPrefix+\"USERNAME\", \"\", \"\")\n\tflag.StringVar(&fPassword, argsPrefix+\"PASSWORD\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./nginxproxymanager_test.go -args \\\n\t--NGINXPROXYMANAGER_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--NGINXPROXYMANAGER_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--NGINXPROXYMANAGER_SERVERURL=\"http://127.0.0.1:81\" \\\n\t--NGINXPROXYMANAGER_USERNAME=\"your-username\" \\\n\t--NGINXPROXYMANAGER_PASSWORD=\"your-password\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"USERNAME: %v\", fUsername),\n\t\t\tfmt.Sprintf(\"PASSWORD: %v\", fPassword),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tAuthMethod:               provider.AUTH_METHOD_PASSWORD,\n\t\t\tUsername:                 fUsername,\n\t\t\tPassword:                 fPassword,\n\t\t\tAllowInsecureConnections: true,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/qiniu-sslcert/qiniu_sslcert.go",
    "content": "package qiniusslcert\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/qiniu/go-sdk/v7/auth\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tqiniusdk \"github.com/certimate-go/certimate/pkg/sdk3rd/qiniu\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 七牛云 AccessKey。\n\tAccessKey string `json:\"accessKey\"`\n\t// 七牛云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *qiniusdk.SslCertManager\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKey, config.SecretKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 生成新证书名（需符合七牛云命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 查询已有证书，避免重复上传\n\tgetSslCertListMarker := \"\"\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tgetSslCertListResp, err := c.sdkClient.GetSslCertList(ctx, getSslCertListMarker, 200)\n\t\tc.logger.Debug(\"sdk request 'sslcert.GetList'\", slog.Any(\"request.marker\", getSslCertListMarker), slog.Any(\"response\", getSslCertListResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'sslcert.GetList': %w\", err)\n\t\t}\n\n\t\tfor _, sslItem := range getSslCertListResp.Certs {\n\t\t\t// 对比证书通用名称\n\t\t\tif !strings.EqualFold(certX509.Subject.CommonName, sslItem.CommonName) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书多域名\n\t\t\tif !slices.Equal(certX509.DNSNames, sslItem.DnsNames) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书有效期\n\t\t\tif certX509.NotBefore.Unix() != sslItem.NotBefore || certX509.NotAfter.Unix() != sslItem.NotAfter {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书公钥算法\n\t\t\tswitch certX509.PublicKeyAlgorithm {\n\t\t\tcase x509.RSA:\n\t\t\t\tif !strings.EqualFold(sslItem.Encrypt, \"RSA\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase x509.ECDSA:\n\t\t\t\tif !strings.EqualFold(sslItem.Encrypt, \"ECDSA\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase x509.Ed25519:\n\t\t\t\tif !strings.EqualFold(sslItem.Encrypt, \"Ed25519\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\t// 未知算法，跳过\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   sslItem.CertID,\n\t\t\t\tCertName: sslItem.Name,\n\t\t\t}, nil\n\t\t}\n\n\t\tif len(getSslCertListResp.Certs) == 0 || getSslCertListResp.Marker == \"\" {\n\t\t\tbreak\n\t\t}\n\n\t\tgetSslCertListMarker = getSslCertListResp.Marker\n\t}\n\n\t// 上传新证书\n\t// REF: https://developer.qiniu.com/fusion/8593/interface-related-certificate\n\tuploadSslCertResp, err := c.sdkClient.UploadSslCert(ctx, certName, certX509.Subject.CommonName, certPEM, privkeyPEM)\n\tc.logger.Debug(\"sdk request 'sslcert.Upload'\", slog.Any(\"response\", uploadSslCertResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'sslcert.Upload': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   uploadSslCertResp.CertID,\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(accessKey, secretKey string) (*qiniusdk.SslCertManager, error) {\n\tif secretKey == \"\" {\n\t\treturn nil, errors.New(\"qiniu: invalid access key\")\n\t}\n\tif secretKey == \"\" {\n\t\treturn nil, errors.New(\"qiniu: invalid secret key\")\n\t}\n\n\tcredential := auth.New(accessKey, secretKey)\n\tclient := qiniusdk.NewSslCertManager(credential)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/qiniu-sslcert/qiniu_sslcert_test.go",
    "content": "package qiniusslcert_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/qiniu-sslcert\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfAccessKey     string\n\tfSecretKey     string\n)\n\nfunc init() {\n\targsPrefix := \"QINIUSSLCERT_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKey, argsPrefix+\"ACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./qiniu_sslcert_test.go -args \\\n\t--QINIUSSLCERT_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--QINIUSSLCERT_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--QINIUSSLCERT_ACCESSKEY=\"your-access-key\" \\\n\t--QINIUSSLCERT_SECRETKEY=\"your-secret-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEY: %v\", fAccessKey),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tAccessKey: fAccessKey,\n\t\t\tSecretKey: fSecretKey,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/rainyun-sslcenter/rainyun_sslcenter.go",
    "content": "package rainyunsslcenter\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\trainyunsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/rainyun\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 雨云 API 密钥。\n\tApiKey string `json:\"ApiKey\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *rainyunsdk.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ApiKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 避免重复上传\n\tif upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM); err != nil {\n\t\treturn nil, err\n\t} else if upok {\n\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\treturn upres, nil\n\t}\n\n\t// SSL 证书上传\n\t// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943046\n\tsslCenterCreateReq := &rainyunsdk.SslCenterCreateRequest{\n\t\tCert: certPEM,\n\t\tKey:  privkeyPEM,\n\t}\n\tsslCenterCreateResp, err := c.sdkClient.SslCenterCreateWithContext(ctx, sslCenterCreateReq)\n\tc.logger.Debug(\"sdk request 'sslcenter.Create'\", slog.Any(\"request\", sslCenterCreateReq), slog.Any(\"response\", sslCenterCreateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'sslcenter.Create': %w\", err)\n\t}\n\n\t// 获取刚刚上传证书 ID\n\tif upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM); err != nil {\n\t\treturn nil, err\n\t} else if !upok {\n\t\treturn nil, errors.New(\"could not find ssl certificate, may be upload failed\")\n\t} else {\n\t\treturn upres, nil\n\t}\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\tcertId, err := strconv.ParseInt(certIdOrName, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// SSL 证书替换操作\n\t// REF: https://s.apifox.cn/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943049\n\tsslCenterUpdateReq := &rainyunsdk.SslCenterUpdateRequest{\n\t\tCert: certPEM,\n\t\tKey:  privkeyPEM,\n\t}\n\tsslCenterUpdateResp, err := c.sdkClient.SslCenterUpdateWithContext(ctx, certId, sslCenterUpdateReq)\n\tc.logger.Debug(\"sdk request 'sslcenter.Update'\", slog.Int64(\"certId\", certId), slog.Any(\"request\", sslCenterUpdateReq), slog.Any(\"response\", sslCenterUpdateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'sslcenter.Update': %w\", err)\n\t}\n\n\treturn &certmgr.OperateResult{}, nil\n}\n\nfunc (c *Certmgr) tryGetResultIfCertExists(ctx context.Context, certPEM string) (*certmgr.UploadResult, bool, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\t// 获取 SSL 证书列表\n\t// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943046\n\t// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943048\n\tsslCenterListPage := 1\n\tsslCenterListPerPage := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, false, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tsslCenterListReq := &rainyunsdk.SslCenterListRequest{\n\t\t\tFilters: &rainyunsdk.SslCenterListFilters{\n\t\t\t\tDomain: &certX509.Subject.CommonName,\n\t\t\t},\n\t\t\tPage:    lo.ToPtr(int32(sslCenterListPage)),\n\t\t\tPerPage: lo.ToPtr(int32(sslCenterListPerPage)),\n\t\t}\n\t\tsslCenterListResp, err := c.sdkClient.SslCenterListWithContext(ctx, sslCenterListReq)\n\t\tc.logger.Debug(\"sdk request 'sslcenter.List'\", slog.Any(\"request\", sslCenterListReq), slog.Any(\"response\", sslCenterListResp))\n\t\tif err != nil {\n\t\t\treturn nil, false, fmt.Errorf(\"failed to execute sdk request 'sslcenter.List': %w\", err)\n\t\t}\n\n\t\tif sslCenterListResp.Data == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, sslItem := range sslCenterListResp.Data.Records {\n\t\t\t// 对比证书的多域名\n\t\t\tif sslItem.Domain != strings.Join(certX509.DNSNames, \", \") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书的有效期\n\t\t\tif sslItem.StartDate != certX509.NotBefore.Unix() || sslItem.ExpireDate != certX509.NotAfter.Unix() {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书内容\n\t\t\tsslCenterGetResp, err := c.sdkClient.SslCenterGetWithContext(ctx, sslItem.ID)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, false, fmt.Errorf(\"failed to execute sdk request 'sslcenter.Get': %w\", err)\n\t\t\t} else {\n\t\t\t\tif !xcert.EqualCertificatesFromPEM(certPEM, sslCenterGetResp.Data.Cert) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId: fmt.Sprintf(\"%d\", sslItem.ID),\n\t\t\t}, true, nil\n\t\t}\n\n\t\tif len(sslCenterListResp.Data.Records) < sslCenterListPerPage {\n\t\t\tbreak\n\t\t}\n\n\t\tsslCenterListPage++\n\t}\n\n\treturn nil, false, nil\n}\n\nfunc createSDKClient(apiKey string) (*rainyunsdk.Client, error) {\n\treturn rainyunsdk.NewClient(apiKey)\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/rainyun-sslcenter/rainyun_sslcenter_test.go",
    "content": "package rainyunsslcenter_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/rainyun-sslcenter\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfApiKey        string\n)\n\nfunc init() {\n\targsPrefix := \"RAINYUNSSLCENTER_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./rainyun_sslcenter_test.go -args \\\n\t--RAINYUNSSLCENTER_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--RAINYUNSSLCENTER_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--RAINYUNSSLCENTER_APIKEY=\"your-api-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tApiKey: fApiKey,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/tencentcloud-ssl/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcssl \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205\"\n)\n\n// This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/ssl/v20191205/client.go\n// to lightweight the vendor packages in the built binary.\ntype SslClient struct {\n\tcommon.Client\n}\n\nfunc NewSslClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *SslClient, err error) {\n\tclient = &SslClient{}\n\tclient.Init(region).\n\t\tWithCredential(credential).\n\t\tWithProfile(clientProfile)\n\treturn\n}\n\nfunc (c *SslClient) UploadCertificate(request *tcssl.UploadCertificateRequest) (response *tcssl.UploadCertificateResponse, err error) {\n\treturn c.UploadCertificateWithContext(context.Background(), request)\n}\n\nfunc (c *SslClient) UploadCertificateWithContext(ctx context.Context, request *tcssl.UploadCertificateRequest) (response *tcssl.UploadCertificateResponse, err error) {\n\tif request == nil {\n\t\trequest = tcssl.NewUploadCertificateRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"ssl\", tcssl.APIVersion, \"UploadCertificate\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"UploadCertificate require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcssl.NewUploadCertificateResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/tencentcloud-ssl/tencentcloud_ssl.go",
    "content": "package tencentcloudssl\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcssl \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl/internal\"\n)\n\ntype CertmgrConfig struct {\n\t// 腾讯云 SecretId。\n\tSecretId string `json:\"secretId\"`\n\t// 腾讯云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 腾讯云接口端点。\n\tEndpoint string `json:\"endpoint,omitempty\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *internal.SslClient\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 上传新证书\n\t// REF: https://cloud.tencent.com/document/api/400/41665\n\tuploadCertificateReq := tcssl.NewUploadCertificateRequest()\n\tuploadCertificateReq.CertificatePublicKey = common.StringPtr(certPEM)\n\tuploadCertificateReq.CertificatePrivateKey = common.StringPtr(privkeyPEM)\n\tuploadCertificateReq.Repeatable = common.BoolPtr(false)\n\tuploadCertificateResp, err := c.sdkClient.UploadCertificate(uploadCertificateReq)\n\tc.logger.Debug(\"sdk request 'ssl.UploadCertificate'\", slog.Any(\"request\", uploadCertificateReq), slog.Any(\"response\", uploadCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'ssl.UploadCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId: *uploadCertificateResp.Response.CertificateId,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(secretId, secretKey, endpoint string) (*internal.SslClient, error) {\n\tcredential := common.NewCredential(secretId, secretKey)\n\n\tcpf := profile.NewClientProfile()\n\tif endpoint != \"\" {\n\t\tcpf.HttpProfile.Endpoint = endpoint\n\t}\n\n\tclient, err := internal.NewSslClient(credential, \"\", cpf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/tencentcloud-ssl/tencentcloud_ssl_test.go",
    "content": "package tencentcloudssl_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfSecretId      string\n\tfSecretKey     string\n)\n\nfunc init() {\n\targsPrefix := \"TENCENTCLOUDSSL_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fSecretId, argsPrefix+\"SECRETID\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./tencentcloud_ssl_test.go -args \\\n\t--TENCENTCLOUDSSL_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--TENCENTCLOUDSSL_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--TENCENTCLOUDSSL_SECRETID=\"your-secret-id\" \\\n\t--TENCENTCLOUDSSL_SECRETKEY=\"your-secret-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SECRETID: %v\", fSecretId),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tSecretId:  fSecretId,\n\t\t\tSecretKey: fSecretKey,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ucloud-ulb/ucloud_ulb.go",
    "content": "package ucloudulb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tucloudsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/ulb\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 优刻得 API 私钥。\n\tPrivateKey string `json:\"privateKey\"`\n\t// 优刻得 API 公钥。\n\tPublicKey string `json:\"publicKey\"`\n\t// 优刻得项目 ID。\n\tProjectId string `json:\"projectId,omitempty\"`\n\t// 优刻得地域。\n\tRegion string `json:\"region\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *ucloudsdk.ULBClient\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 避免重复上传\n\tif upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM, privkeyPEM); err != nil {\n\t\treturn nil, err\n\t} else if upok {\n\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\treturn upres, nil\n\t}\n\n\t// 提取服务器证书和中间证书\n\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to extract certs: %w\", err)\n\t}\n\n\t// 生成新证书名（需符合优刻得命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 创建 SSL 证书\n\t// REF: https://docs.ucloud.cn/api/ulb-api/create_ssl\n\tcreateSSLReq := c.sdkClient.NewCreateSSLRequest()\n\tcreateSSLReq.SSLName = ucloud.String(certName)\n\tcreateSSLReq.SSLType = ucloud.String(\"Pem\")\n\tcreateSSLReq.UserCert = ucloud.String(serverCertPEM)\n\tcreateSSLReq.CaCert = ucloud.String(intermediaCertPEM)\n\tcreateSSLReq.PrivateKey = ucloud.String(privkeyPEM)\n\tcreateSSLResp, err := c.sdkClient.CreateSSL(createSSLReq)\n\tc.logger.Debug(\"sdk request 'ulb.CreateSSL'\", slog.Any(\"request\", createSSLReq), slog.Any(\"response\", createSSLResp))\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   createSSLResp.SSLId,\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc (c *Certmgr) tryGetResultIfCertExists(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, bool, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\t// 获取 SSL 证书信息\n\t// REF: https://docs.ucloud.cn/api/ulb-api/describe_ssl\n\tdescribeSSLOffset := 0\n\tdescribeSSLLimit := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, false, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeSSLReq := c.sdkClient.NewDescribeSSLRequest()\n\t\tdescribeSSLReq.Offset = ucloud.Int(describeSSLOffset)\n\t\tdescribeSSLReq.Limit = ucloud.Int(describeSSLLimit)\n\t\tdescribeSSLResp, err := c.sdkClient.DescribeSSL(describeSSLReq)\n\t\tc.logger.Debug(\"sdk request 'ulb.DescribeSSL'\", slog.Any(\"request\", describeSSLReq), slog.Any(\"response\", describeSSLResp))\n\t\tif err != nil {\n\t\t\treturn nil, false, fmt.Errorf(\"failed to execute sdk request 'ulb.DescribeSSL': %w\", err)\n\t\t}\n\n\t\tfor _, sslItem := range describeSSLResp.DataSet {\n\t\t\t// 对比证书有效期\n\t\t\tif int64(sslItem.NotBefore) != certX509.NotBefore.Unix() || int64(sslItem.NotAfter) != certX509.NotAfter.Unix() {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书及私钥内容\n\t\t\t// 按照“网站证书、私钥、中间证书”的方式拼接\n\t\t\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t} else {\n\t\t\t\toldSSLContent := sslItem.SSLContent\n\t\t\t\toldSSLContent = strings.ReplaceAll(oldSSLContent, \"\\r\", \"\")\n\t\t\t\toldSSLContent = strings.ReplaceAll(oldSSLContent, \"\\n\", \"\")\n\t\t\t\toldSSLContent = strings.ReplaceAll(oldSSLContent, \"\\t\", \"\")\n\t\t\t\toldSSLContent = strings.ReplaceAll(oldSSLContent, \" \", \"\")\n\n\t\t\t\tnewSSLContent := serverCertPEM + privkeyPEM + intermediaCertPEM\n\t\t\t\tnewSSLContent = strings.ReplaceAll(newSSLContent, \"\\r\", \"\")\n\t\t\t\tnewSSLContent = strings.ReplaceAll(newSSLContent, \"\\n\", \"\")\n\t\t\t\tnewSSLContent = strings.ReplaceAll(newSSLContent, \"\\t\", \"\")\n\t\t\t\tnewSSLContent = strings.ReplaceAll(newSSLContent, \" \", \"\")\n\n\t\t\t\tif oldSSLContent != newSSLContent {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   sslItem.SSLId,\n\t\t\t\tCertName: sslItem.SSLName,\n\t\t\t}, true, nil\n\t\t}\n\n\t\tif len(describeSSLResp.DataSet) < describeSSLLimit {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeSSLOffset += describeSSLLimit\n\t}\n\n\treturn nil, false, nil\n}\n\nfunc createSDKClient(privateKey, publicKey, projectId, region string) (*ucloudsdk.ULBClient, error) {\n\tif privateKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"ucloud: invalid private key\")\n\t}\n\tif publicKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"ucloud: invalid public key\")\n\t}\n\n\tcfg := ucloud.NewConfig()\n\tcfg.ProjectId = projectId\n\tcfg.Region = region\n\n\tcredential := auth.NewCredential()\n\tcredential.PrivateKey = privateKey\n\tcredential.PublicKey = publicKey\n\n\tclient := ucloudsdk.NewClient(&cfg, &credential)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ucloud-ulb/ucloud_ulb_test.go",
    "content": "package ucloudulb_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ucloud-ulb\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfPrivateKey    string\n\tfPublicKey     string\n\tfRegion        string\n)\n\nfunc init() {\n\targsPrefix := \"UCLOUDULB_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fPrivateKey, argsPrefix+\"PRIVATEKEY\", \"\", \"\")\n\tflag.StringVar(&fPublicKey, argsPrefix+\"PUBLICKEY\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ucloud_ulb_test.go -args \\\n\t--UCLOUDULB_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--UCLOUDULB_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--UCLOUDULB_PRIVATEKEY=\"your-private-key\" \\\n\t--UCLOUDULB_PUBLICKEY=\"your-public-key\" \\\n\t--UCLOUDULB_REGION=\"cn-bj2\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"PRIVATEKEY: %v\", fPrivateKey),\n\t\t\tfmt.Sprintf(\"PUBLICKEY: %v\", fPublicKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tPrivateKey: fPrivateKey,\n\t\t\tPublicKey:  fPublicKey,\n\t\t\tRegion:     fRegion,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ucloud-upathx/ucloud_upathx.go",
    "content": "package ucloudulb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/ucloud/ucloud-sdk-go/services/uaccount\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tucloudsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/upathx\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 优刻得 API 私钥。\n\tPrivateKey string `json:\"privateKey\"`\n\t// 优刻得 API 公钥。\n\tPublicKey string `json:\"publicKey\"`\n\t// 优刻得项目 ID。\n\tProjectId string `json:\"projectId,omitempty\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *ucloudsdk.UPathXClient\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 避免重复上传\n\tif upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM, privkeyPEM); err != nil {\n\t\treturn nil, err\n\t} else if upok {\n\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\treturn upres, nil\n\t}\n\n\t// 提取服务器证书和中间证书\n\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to extract certs: %w\", err)\n\t}\n\n\t// 生成新证书名（需符合优刻得命名规则）\n\tcertName := fmt.Sprintf(\"certimate_%d\", time.Now().UnixMilli())\n\n\t// 创建证书\n\t// REF: https://docs.ucloud.cn/api/pathx-api/create_path_xssl\n\tcreatePathXSSLReq := c.sdkClient.NewCreatePathXSSLRequest()\n\tcreatePathXSSLReq.SSLName = ucloud.String(certName)\n\tcreatePathXSSLReq.SSLType = ucloud.String(\"Pem\")\n\tcreatePathXSSLReq.UserCert = ucloud.String(serverCertPEM)\n\tcreatePathXSSLReq.CACert = ucloud.String(intermediaCertPEM)\n\tcreatePathXSSLReq.PrivateKey = ucloud.String(privkeyPEM)\n\tcreatePathXSSLResp, err := c.sdkClient.CreatePathXSSL(createPathXSSLReq)\n\tc.logger.Debug(\"sdk request 'pathx.CreatePathXSSL'\", slog.Any(\"request\", createPathXSSLReq), slog.Any(\"response\", createPathXSSLResp))\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   createPathXSSLResp.SSLId,\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc (c *Certmgr) tryGetResultIfCertExists(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, bool, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\t// 获取证书信息\n\t// REF: https://docs.ucloud.cn/api/pathx-api/describe_path_xssl\n\tdescribePathXSSLOffset := 0\n\tdescribePathXSSLLimit := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, false, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribePathXSSLReq := c.sdkClient.NewDescribePathXSSLRequest()\n\t\tdescribePathXSSLReq.Offset = ucloud.Int(describePathXSSLOffset)\n\t\tdescribePathXSSLReq.Limit = ucloud.Int(describePathXSSLLimit)\n\t\tdescribePathXSSLResp, err := c.sdkClient.DescribePathXSSL(describePathXSSLReq)\n\t\tc.logger.Debug(\"sdk request 'pathx.DescribePathXSSL'\", slog.Any(\"request\", describePathXSSLReq), slog.Any(\"response\", describePathXSSLResp))\n\t\tif err != nil {\n\t\t\treturn nil, false, fmt.Errorf(\"failed to execute sdk request 'pathx.DescribePathXSSL': %w\", err)\n\t\t}\n\n\t\tfor _, sslItem := range describePathXSSLResp.DataSet {\n\t\t\t// 对比证书有效期\n\t\t\tif int64(sslItem.ExpireTime) != certX509.NotAfter.Unix() {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书及私钥内容\n\t\t\t// 按照“私钥、证书链”的方式拼接\n\t\t\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t} else {\n\t\t\t\toldSSLContent := sslItem.SSLContent\n\t\t\t\toldSSLContent = strings.ReplaceAll(oldSSLContent, \"\\r\", \"\")\n\t\t\t\toldSSLContent = strings.ReplaceAll(oldSSLContent, \"\\n\", \"\")\n\t\t\t\toldSSLContent = strings.ReplaceAll(oldSSLContent, \"\\t\", \"\")\n\t\t\t\toldSSLContent = strings.ReplaceAll(oldSSLContent, \" \", \"\")\n\n\t\t\t\tnewSSLContent := privkeyPEM + serverCertPEM + intermediaCertPEM\n\t\t\t\tnewSSLContent = strings.ReplaceAll(newSSLContent, \"\\r\", \"\")\n\t\t\t\tnewSSLContent = strings.ReplaceAll(newSSLContent, \"\\n\", \"\")\n\t\t\t\tnewSSLContent = strings.ReplaceAll(newSSLContent, \"\\t\", \"\")\n\t\t\t\tnewSSLContent = strings.ReplaceAll(newSSLContent, \" \", \"\")\n\n\t\t\t\tif oldSSLContent != newSSLContent {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   sslItem.SSLId,\n\t\t\t\tCertName: sslItem.SSLName,\n\t\t\t}, true, nil\n\t\t}\n\n\t\tif len(describePathXSSLResp.DataSet) < describePathXSSLLimit {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribePathXSSLOffset += describePathXSSLLimit\n\t}\n\n\treturn nil, false, nil\n}\n\nfunc createSDKClient(privateKey, publicKey, projectId string) (*ucloudsdk.UPathXClient, error) {\n\tif privateKey == \"\" {\n\t\treturn nil, errors.New(\"ucloud: invalid private key\")\n\t}\n\tif publicKey == \"\" {\n\t\treturn nil, errors.New(\"ucloud: invalid public key\")\n\t}\n\n\tcfg := ucloud.NewConfig()\n\tcfg.ProjectId = projectId\n\n\t// PathX 相关接口要求必传 ProjectId 参数\n\tif cfg.ProjectId == \"\" {\n\t\tdefaultProjectId, err := getSDKDefaultProjectId(privateKey, publicKey)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tcfg.ProjectId = defaultProjectId\n\t}\n\n\tcredential := auth.NewCredential()\n\tcredential.PrivateKey = privateKey\n\tcredential.PublicKey = publicKey\n\n\tclient := ucloudsdk.NewClient(&cfg, &credential)\n\treturn client, nil\n}\n\nfunc getSDKDefaultProjectId(privateKey, publicKey string) (string, error) {\n\tcfg := ucloud.NewConfig()\n\n\tcredential := auth.NewCredential()\n\tcredential.PrivateKey = privateKey\n\tcredential.PublicKey = publicKey\n\n\tclient := uaccount.NewClient(&cfg, &credential)\n\n\trequest := client.NewGetProjectListRequest()\n\tresponse, err := client.GetProjectList(request)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, projectItem := range response.ProjectSet {\n\t\tif projectItem.IsDefault {\n\t\t\treturn projectItem.ProjectId, nil\n\t\t}\n\t}\n\n\treturn \"\", errors.New(\"ucloud: no default project found\")\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ucloud-upathx/ucloud_upathx_test.go",
    "content": "package ucloudulb_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ucloud-upathx\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfPrivateKey    string\n\tfPublicKey     string\n)\n\nfunc init() {\n\targsPrefix := \"UCLOUDUPATHX_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fPrivateKey, argsPrefix+\"PRIVATEKEY\", \"\", \"\")\n\tflag.StringVar(&fPublicKey, argsPrefix+\"PUBLICKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ucloud_upathx_test.go -args \\\n\t--UCLOUDUPATHX_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--UCLOUDUPATHX_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--UCLOUDUPATHX_PRIVATEKEY=\"your-private-key\" \\\n\t--UCLOUDUPATHX_PUBLICKEY=\"your-public-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"PRIVATEKEY: %v\", fPrivateKey),\n\t\t\tfmt.Sprintf(\"PUBLICKEY: %v\", fPublicKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tPrivateKey: fPrivateKey,\n\t\t\tPublicKey:  fPublicKey,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ucloud-ussl/ucloud_ussl.go",
    "content": "package ucloudussl\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tucloudsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/ussl\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 优刻得 API 私钥。\n\tPrivateKey string `json:\"privateKey\"`\n\t// 优刻得 API 公钥。\n\tPublicKey string `json:\"publicKey\"`\n\t// 优刻得项目 ID。\n\tProjectId string `json:\"projectId,omitempty\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *ucloudsdk.USSLClient\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 生成新证书名（需符合优刻得命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 生成优刻得所需的证书参数\n\tcertPEMBase64 := base64.StdEncoding.EncodeToString([]byte(certPEM))\n\tprivkeyPEMBase64 := base64.StdEncoding.EncodeToString([]byte(privkeyPEM))\n\tcertMd5 := md5.Sum([]byte(certPEMBase64 + privkeyPEMBase64))\n\tcertMd5Hex := hex.EncodeToString(certMd5[:])\n\n\t// 上传托管证书\n\t// REF: https://docs.ucloud.cn/api/usslcertificate-api/upload_normal_certificate\n\tuploadNormalCertificateReq := c.sdkClient.NewUploadNormalCertificateRequest()\n\tuploadNormalCertificateReq.CertificateName = ucloud.String(certName)\n\tuploadNormalCertificateReq.SslPublicKey = ucloud.String(certPEMBase64)\n\tuploadNormalCertificateReq.SslPrivateKey = ucloud.String(privkeyPEMBase64)\n\tuploadNormalCertificateReq.SslMD5 = ucloud.String(certMd5Hex)\n\tuploadNormalCertificateResp, err := c.sdkClient.UploadNormalCertificate(uploadNormalCertificateReq)\n\tc.logger.Debug(\"sdk request 'ussl.UploadNormalCertificate'\", slog.Any(\"request\", uploadNormalCertificateReq), slog.Any(\"response\", uploadNormalCertificateResp))\n\tif err != nil {\n\t\tif uploadNormalCertificateResp != nil && uploadNormalCertificateResp.GetRetCode() == 80035 {\n\t\t\tif upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t} else if !upok {\n\t\t\t\treturn nil, errors.New(\"could not find ssl certificate, may be upload failed\")\n\t\t\t} else {\n\t\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\t\treturn upres, nil\n\t\t\t}\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'ussl.UploadNormalCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   fmt.Sprintf(\"%d\", uploadNormalCertificateResp.CertificateID),\n\t\tCertName: certName,\n\t\tExtendedData: map[string]any{\n\t\t\t\"ResourceId\": uploadNormalCertificateResp.LongResourceID,\n\t\t},\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc (c *Certmgr) tryGetResultIfCertExists(ctx context.Context, certPEM string) (*certmgr.UploadResult, bool, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\t// 查询用户证书列表\n\t// REF: https://docs.ucloud.cn/api/usslcertificate-api/get_certificate_list\n\t// REF: https://docs.ucloud.cn/api/usslcertificate-api/download_certificate\n\tgetCertificateListPage := 1\n\tgetCertificateListLimit := 1000\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, false, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tgetCertificateListReq := c.sdkClient.NewGetCertificateListRequest()\n\t\tgetCertificateListReq.Mode = ucloud.String(\"trust\")\n\t\tgetCertificateListReq.Domain = ucloud.String(certX509.Subject.CommonName)\n\t\tgetCertificateListReq.Sort = ucloud.String(\"2\")\n\t\tgetCertificateListReq.Page = ucloud.Int(getCertificateListPage)\n\t\tgetCertificateListReq.PageSize = ucloud.Int(getCertificateListLimit)\n\t\tgetCertificateListResp, err := c.sdkClient.GetCertificateList(getCertificateListReq)\n\t\tc.logger.Debug(\"sdk request 'ussl.GetCertificateList'\", slog.Any(\"request\", getCertificateListReq), slog.Any(\"response\", getCertificateListResp))\n\t\tif err != nil {\n\t\t\treturn nil, false, fmt.Errorf(\"failed to execute sdk request 'ussl.GetCertificateList': %w\", err)\n\t\t}\n\n\t\tfor _, certItem := range getCertificateListResp.CertificateList {\n\t\t\t// 优刻得未提供可唯一标识证书的字段，只能通过多个字段尝试对比来判断是否为同一证书\n\t\t\t// 先分别对比证书的多域名、品牌、有效期，再对比签名算法\n\n\t\t\tif len(certX509.DNSNames) == 0 || certItem.Domains != strings.Join(certX509.DNSNames, \",\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif len(certX509.Issuer.Organization) == 0 || certItem.Brand != certX509.Issuer.Organization[0] {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif int64(certItem.NotBefore) != certX509.NotBefore.UnixMilli() || int64(certItem.NotAfter) != certX509.NotAfter.UnixMilli() {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tgetCertificateDetailInfoReq := c.sdkClient.NewGetCertificateDetailInfoRequest()\n\t\t\tgetCertificateDetailInfoReq.CertificateID = ucloud.Int(certItem.CertificateID)\n\t\t\tgetCertificateDetailInfoResp, err := c.sdkClient.GetCertificateDetailInfo(getCertificateDetailInfoReq)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, false, fmt.Errorf(\"failed to execute sdk request 'ussl.GetCertificateDetailInfo': %w\", err)\n\t\t\t}\n\n\t\t\tswitch certX509.SignatureAlgorithm {\n\t\t\tcase x509.SHA256WithRSA:\n\t\t\t\tif !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, \"SHA256-RSA\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase x509.SHA384WithRSA:\n\t\t\t\tif !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, \"SHA384-RSA\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase x509.SHA512WithRSA:\n\t\t\t\tif !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, \"SHA512-RSA\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase x509.SHA256WithRSAPSS:\n\t\t\t\tif !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, \"SHA256-RSAPSS\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase x509.SHA384WithRSAPSS:\n\t\t\t\tif !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, \"SHA384-RSAPSS\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase x509.SHA512WithRSAPSS:\n\t\t\t\tif !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, \"SHA512-RSAPSS\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase x509.ECDSAWithSHA256:\n\t\t\t\tif !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, \"ECDSA-SHA256\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase x509.ECDSAWithSHA384:\n\t\t\t\tif !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, \"ECDSA-SHA384\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase x509.ECDSAWithSHA512:\n\t\t\t\tif !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, \"ECDSA-SHA512\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\t// 未知签名算法，跳过\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   fmt.Sprintf(\"%d\", certItem.CertificateID),\n\t\t\t\tCertName: certItem.Name,\n\t\t\t\tExtendedData: map[string]any{\n\t\t\t\t\t\"ResourceId\": certItem.CertificateSN,\n\t\t\t\t},\n\t\t\t}, true, nil\n\t\t}\n\n\t\tif len(getCertificateListResp.CertificateList) < getCertificateListLimit {\n\t\t\tbreak\n\t\t}\n\n\t\tgetCertificateListPage++\n\t}\n\n\treturn nil, false, nil\n}\n\nfunc createSDKClient(privateKey, publicKey, projectId string) (*ucloudsdk.USSLClient, error) {\n\tif privateKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"ucloud: invalid private key\")\n\t}\n\tif publicKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"ucloud: invalid public key\")\n\t}\n\n\tcfg := ucloud.NewConfig()\n\tcfg.ProjectId = projectId\n\n\tcredential := auth.NewCredential()\n\tcredential.PrivateKey = privateKey\n\tcredential.PublicKey = publicKey\n\n\tclient := ucloudsdk.NewClient(&cfg, &credential)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/ucloud-ussl/ucloud_ussl_test.go",
    "content": "package ucloudussl_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ucloud-ussl\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfPrivateKey    string\n\tfPublicKey     string\n)\n\nfunc init() {\n\targsPrefix := \"UCLOUDUSSL_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fPrivateKey, argsPrefix+\"PRIVATEKEY\", \"\", \"\")\n\tflag.StringVar(&fPublicKey, argsPrefix+\"PUBLICKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ucloud_ussl_test.go -args \\\n\t--UCLOUDUSSL_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--UCLOUDUSSL_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--UCLOUDUSSL_PRIVATEKEY=\"your-private-key\" \\\n\t--UCLOUDUSSL_PUBLICKEY=\"your-public-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"PRIVATEKEY: %v\", fPrivateKey),\n\t\t\tfmt.Sprintf(\"PUBLICKEY: %v\", fPublicKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tPrivateKey: fPrivateKey,\n\t\t\tPublicKey:  fPublicKey,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/upyun-ssl/upyun_ssl.go",
    "content": "package upyunssl\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tupyunsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/upyun/console\"\n)\n\ntype CertmgrConfig struct {\n\t// 又拍云账号用户名。\n\tUsername string `json:\"username\"`\n\t// 又拍云账号密码。\n\tPassword string `json:\"password\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *upyunsdk.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.Username, config.Password)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 上传证书\n\tuploadHttpsCertificateReq := &upyunsdk.UploadHttpsCertificateRequest{\n\t\tCertificate: certPEM,\n\t\tPrivateKey:  privkeyPEM,\n\t}\n\tuploadHttpsCertificateResp, err := c.sdkClient.UploadHttpsCertificateWithContext(ctx, uploadHttpsCertificateReq)\n\tc.logger.Debug(\"sdk request 'console.UploadHttpsCertificate'\", slog.Any(\"response\", uploadHttpsCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'console.UploadHttpsCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId: uploadHttpsCertificateResp.Data.Result.CertificateId,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(username, password string) (*upyunsdk.Client, error) {\n\treturn upyunsdk.NewClient(username, password)\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/upyun-ssl/upyun_ssl_test.go",
    "content": "package upyunssl_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/upyun-ssl\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfUsername      string\n\tfPassword      string\n)\n\nfunc init() {\n\targsPrefix := \"UPYUNSSL_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fUsername, argsPrefix+\"USERNAME\", \"\", \"\")\n\tflag.StringVar(&fPassword, argsPrefix+\"PASSWORD\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./upyun_ssl_test.go -args \\\n\t--UPYUNSSL_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--UPYUNSSL_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--UPYUNSSL_USERNAME=\"your-username\" \\\n\t--UPYUNSSL_PASSWORD=\"your-password\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"USERNAME: %v\", fUsername),\n\t\t\tfmt.Sprintf(\"PASSWORD: %v\", fPassword),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tUsername: fUsername,\n\t\t\tPassword: fPassword,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/volcengine-cdn/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"github.com/volcengine/volcengine-go-sdk/service/cdn\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/client\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/client/metadata\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/corehandlers\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/request\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/signer/volc\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/volcenginequery\"\n)\n\n// This is a partial copy of https://github.com/volcengine/volcengine-go-sdk/blob/master/service/cdn/service_cdn.go\n// to lightweight the vendor packages in the built binary.\ntype CdnClient struct {\n\t*client.Client\n}\n\nfunc NewCdnClient(p client.ConfigProvider, cfgs ...*volcengine.Config) *CdnClient {\n\tc := p.ClientConfig(cdn.EndpointsID, cfgs...)\n\treturn newCdnClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, c.SigningName)\n}\n\nfunc newCdnClient(cfg volcengine.Config, handlers request.Handlers, endpoint, signingRegion, signingName string) *CdnClient {\n\tsvc := &CdnClient{\n\t\tClient: client.New(\n\t\t\tcfg,\n\t\t\tmetadata.ClientInfo{\n\t\t\t\tServiceName:   cdn.ServiceName,\n\t\t\t\tServiceID:     cdn.ServiceID,\n\t\t\t\tSigningName:   signingName,\n\t\t\t\tSigningRegion: signingRegion,\n\t\t\t\tEndpoint:      endpoint,\n\t\t\t\tAPIVersion:    \"2021-03-01\",\n\t\t\t},\n\t\t\thandlers,\n\t\t),\n\t}\n\n\tsvc.Handlers.Build.PushBackNamed(corehandlers.SDKVersionUserAgentHandler)\n\tsvc.Handlers.Build.PushBackNamed(corehandlers.AddHostExecEnvUserAgentHandler)\n\tsvc.Handlers.Sign.PushBackNamed(volc.SignRequestHandler)\n\tsvc.Handlers.Build.PushBackNamed(volcenginequery.BuildHandler)\n\tsvc.Handlers.Unmarshal.PushBackNamed(volcenginequery.UnmarshalHandler)\n\tsvc.Handlers.UnmarshalMeta.PushBackNamed(volcenginequery.UnmarshalMetaHandler)\n\tsvc.Handlers.UnmarshalError.PushBackNamed(volcenginequery.UnmarshalErrorHandler)\n\n\treturn svc\n}\n\nfunc (c *CdnClient) newRequest(op *request.Operation, params, data interface{}) *request.Request {\n\treq := c.NewRequest(op, params, data)\n\n\treturn req\n}\n\nfunc (c *CdnClient) AddCertificate(input *cdn.AddCertificateInput) (*cdn.AddCertificateOutput, error) {\n\treq, out := c.AddCertificateRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *CdnClient) AddCertificateRequest(input *cdn.AddCertificateInput) (req *request.Request, output *cdn.AddCertificateOutput) {\n\top := &request.Operation{\n\t\tName:       \"AddCertificate\",\n\t\tHTTPMethod: \"POST\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &cdn.AddCertificateInput{}\n\t}\n\n\toutput = &cdn.AddCertificateOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treq.HTTPRequest.Header.Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\n\treturn\n}\n\nfunc (c *CdnClient) ListCertInfo(input *cdn.ListCertInfoInput) (*cdn.ListCertInfoOutput, error) {\n\treq, out := c.ListCertInfoRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *CdnClient) ListCertInfoRequest(input *cdn.ListCertInfoInput) (req *request.Request, output *cdn.ListCertInfoOutput) {\n\top := &request.Operation{\n\t\tName:       \"ListCertInfo\",\n\t\tHTTPMethod: \"POST\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &cdn.ListCertInfoInput{}\n\t}\n\n\toutput = &cdn.ListCertInfoOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treq.HTTPRequest.Header.Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/volcengine-cdn/volcengine_cdn.go",
    "content": "package volcenginecdn\n\nimport (\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\tvecdn \"github.com/volcengine/volcengine-go-sdk/service/cdn\"\n\tve \"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\tvesession \"github.com/volcengine/volcengine-go-sdk/volcengine/session\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-cdn/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 火山引擎 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 火山引擎 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *internal.CdnClient\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 查询证书列表，避免重复上传\n\t// REF: https://www.volcengine.com/docs/6454/125709\n\tlistCertInfoPageNum := 1\n\tlistCertInfoPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistCertInfoReq := &vecdn.ListCertInfoInput{\n\t\t\tSource:   ve.String(\"volc_cert_center\"),\n\t\t\tPageNum:  ve.Int32(int32(listCertInfoPageNum)),\n\t\t\tPageSize: ve.Int32(int32(listCertInfoPageSize)),\n\t\t}\n\t\tlistCertInfoResp, err := c.sdkClient.ListCertInfo(listCertInfoReq)\n\t\tc.logger.Debug(\"sdk request 'cdn.ListCertInfo'\", slog.Any(\"request\", listCertInfoReq), slog.Any(\"response\", listCertInfoResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.ListCertInfo': %w\", err)\n\t\t}\n\n\t\tfor _, certItem := range listCertInfoResp.CertInfo {\n\t\t\t// 对比证书 SHA-1 摘要\n\t\t\tfingerprintSha1 := sha1.Sum(certX509.Raw)\n\t\t\tif !strings.EqualFold(hex.EncodeToString(fingerprintSha1[:]), ve.StringValue(certItem.CertFingerprint.Sha1)) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书 SHA-256 摘要\n\t\t\tfingerprintSha256 := sha256.Sum256(certX509.Raw)\n\t\t\tif !strings.EqualFold(hex.EncodeToString(fingerprintSha256[:]), ve.StringValue(certItem.CertFingerprint.Sha256)) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   ve.StringValue(certItem.CertId),\n\t\t\t\tCertName: ve.StringValue(certItem.Desc),\n\t\t\t}, nil\n\t\t}\n\n\t\tif len(listCertInfoResp.CertInfo) < listCertInfoPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tlistCertInfoPageNum++\n\t}\n\n\t// 生成新证书名（需符合火山引擎命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 上传新证书\n\t// REF: https://www.volcengine.com/docs/6454/1245763\n\taddCertificateReq := &vecdn.AddCertificateInput{\n\t\tSource:      ve.String(\"volc_cert_center\"),\n\t\tCertificate: ve.String(certPEM),\n\t\tPrivateKey:  ve.String(privkeyPEM),\n\t\tDesc:        ve.String(certName),\n\t}\n\taddCertificateResp, err := c.sdkClient.AddCertificate(addCertificateReq)\n\tc.logger.Debug(\"sdk request 'cdn.AddCertificate'\", slog.Any(\"request\", addCertificateResp), slog.Any(\"response\", addCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.AddCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   ve.StringValue(addCertificateResp.CertId),\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret string) (*internal.CdnClient, error) {\n\tconfig := ve.NewConfig().\n\t\tWithAkSk(accessKeyId, accessKeySecret).\n\t\tWithRegion(\"cn-north-1\")\n\n\tsession, err := vesession.NewSession(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := internal.NewCdnClient(session)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/volcengine-certcenter/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"github.com/volcengine/volcengine-go-sdk/service/certificateservice\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/client\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/client/metadata\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/corehandlers\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/request\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/signer/volc\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/volcenginequery\"\n)\n\n// This is a partial copy of https://github.com/volcengine/volcengine-go-sdk/blob/master/service/certificateservice/service_certificateservice.go\n// to lightweight the vendor packages in the built binary.\ntype CertificateserviceClient struct {\n\t*client.Client\n}\n\nfunc NewCertificateserviceClient(p client.ConfigProvider, cfgs ...*volcengine.Config) *CertificateserviceClient {\n\tc := p.ClientConfig(certificateservice.EndpointsID, cfgs...)\n\treturn newCertificateserviceClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, c.SigningName)\n}\n\nfunc newCertificateserviceClient(cfg volcengine.Config, handlers request.Handlers, endpoint, signingRegion, signingName string) *CertificateserviceClient {\n\tsvc := &CertificateserviceClient{\n\t\tClient: client.New(\n\t\t\tcfg,\n\t\t\tmetadata.ClientInfo{\n\t\t\t\tServiceName:   certificateservice.ServiceName,\n\t\t\t\tServiceID:     certificateservice.ServiceID,\n\t\t\t\tSigningName:   signingName,\n\t\t\t\tSigningRegion: signingRegion,\n\t\t\t\tEndpoint:      endpoint,\n\t\t\t\tAPIVersion:    \"2024-10-01\",\n\t\t\t},\n\t\t\thandlers,\n\t\t),\n\t}\n\n\tsvc.Handlers.Build.PushBackNamed(corehandlers.SDKVersionUserAgentHandler)\n\tsvc.Handlers.Build.PushBackNamed(corehandlers.AddHostExecEnvUserAgentHandler)\n\tsvc.Handlers.Sign.PushBackNamed(volc.SignRequestHandler)\n\tsvc.Handlers.Build.PushBackNamed(volcenginequery.BuildHandler)\n\tsvc.Handlers.Unmarshal.PushBackNamed(volcenginequery.UnmarshalHandler)\n\tsvc.Handlers.UnmarshalMeta.PushBackNamed(volcenginequery.UnmarshalMetaHandler)\n\tsvc.Handlers.UnmarshalError.PushBackNamed(volcenginequery.UnmarshalErrorHandler)\n\n\treturn svc\n}\n\nfunc (c *CertificateserviceClient) newRequest(op *request.Operation, params, data interface{}) *request.Request {\n\treq := c.NewRequest(op, params, data)\n\n\treturn req\n}\n\nfunc (c *CertificateserviceClient) ImportCertificate(input *certificateservice.ImportCertificateInput) (*certificateservice.ImportCertificateOutput, error) {\n\treq, out := c.ImportCertificateRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *CertificateserviceClient) ImportCertificateRequest(input *certificateservice.ImportCertificateInput) (req *request.Request, output *certificateservice.ImportCertificateOutput) {\n\top := &request.Operation{\n\t\tName:       \"ImportCertificate\",\n\t\tHTTPMethod: \"POST\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &certificateservice.ImportCertificateInput{}\n\t}\n\n\toutput = &certificateservice.ImportCertificateOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treq.HTTPRequest.Header.Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/volcengine-certcenter/volcengine_certcenter.go",
    "content": "package volcenginecertcenter\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\tvecs \"github.com/volcengine/volcengine-go-sdk/service/certificateservice\"\n\tve \"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\tvesession \"github.com/volcengine/volcengine-go-sdk/volcengine/session\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter/internal\"\n)\n\ntype CertmgrConfig struct {\n\t// 火山引擎 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 火山引擎 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 火山引擎地域。\n\tRegion string `json:\"region\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *internal.CertificateserviceClient\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 上传证书\n\t// REF: https://www.volcengine.com/docs/6638/1365580\n\timportCertificateReq := &vecs.ImportCertificateInput{\n\t\tCertificateInfo: &vecs.CertificateInfoForImportCertificateInput{\n\t\t\tCertificateChain: ve.String(certPEM),\n\t\t\tPrivateKey:       ve.String(privkeyPEM),\n\t\t},\n\t\tRepeatable: ve.Bool(false),\n\t}\n\timportCertificateResp, err := c.sdkClient.ImportCertificate(importCertificateReq)\n\tc.logger.Debug(\"sdk request 'certcenter.ImportCertificate'\", slog.Any(\"request\", importCertificateReq), slog.Any(\"response\", importCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'certcenter.ImportCertificate': %w\", err)\n\t}\n\n\tvar sslId string\n\tif importCertificateResp.InstanceId != nil && *importCertificateResp.InstanceId != \"\" {\n\t\tsslId = *importCertificateResp.InstanceId\n\t}\n\tif importCertificateResp.RepeatId != nil && *importCertificateResp.RepeatId != \"\" {\n\t\tsslId = *importCertificateResp.RepeatId\n\t}\n\n\tif sslId == \"\" {\n\t\treturn nil, errors.New(\"received empty certificate id, both `InstanceId` and `RepeatId` are empty\")\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId: sslId,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.CertificateserviceClient, error) {\n\tif region == \"\" {\n\t\tregion = \"cn-beijing\" // 证书中心默认区域：北京\n\t}\n\n\tconfig := ve.NewConfig().\n\t\tWithAkSk(accessKeyId, accessKeySecret).\n\t\tWithRegion(region)\n\n\tsession, err := vesession.NewSession(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := internal.NewCertificateserviceClient(session)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/volcengine-certcenter/volcengine_certcenter_test.go",
    "content": "package volcenginecertcenter_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n)\n\nfunc init() {\n\targsPrefix := \"VOLCENGINECERTCENTER_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./volcengine_certcenter_test.go -args \\\n\t--VOLCENGINECERTCENTER_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--VOLCENGINECERTCENTER_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--VOLCENGINECERTCENTER_ACCESSKEYID=\"your-access-key-id\" \\\n\t--VOLCENGINECERTCENTER_ACCESSKEYSECRET=\"your-access-key-secret\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/volcengine-live/volcengine_live.go",
    "content": "package volcenginelive\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\tvelive \"github.com/volcengine/volc-sdk-golang/service/live/v20230101\"\n\tve \"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 火山引擎 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 火山引擎 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *velive.Live\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient := velive.NewInstance()\n\tclient.SetAccessKey(config.AccessKeyId)\n\tclient.SetSecretKey(config.AccessKeySecret)\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 查询证书列表，避免重复上传\n\t// REF: https://www.volcengine.com/docs/6469/1186278#%E6%9F%A5%E8%AF%A2%E8%AF%81%E4%B9%A6%E5%88%97%E8%A1%A8\n\tlistCertReq := &velive.ListCertV2Body{}\n\tlistCertResp, err := c.sdkClient.ListCertV2(ctx, listCertReq)\n\tc.logger.Debug(\"sdk request 'live.ListCertV2'\", slog.Any(\"request\", listCertReq), slog.Any(\"response\", listCertResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'live.ListCertV2': %w\", err)\n\t}\n\tif listCertResp.Result.CertList != nil {\n\t\tfor _, certItem := range listCertResp.Result.CertList {\n\t\t\t// 查询证书详细信息\n\t\t\t// REF: https://www.volcengine.com/docs/6469/1186278#%E6%9F%A5%E7%9C%8B%E8%AF%81%E4%B9%A6%E8%AF%A6%E6%83%85\n\t\t\tdescribeCertDetailSecretReq := &velive.DescribeCertDetailSecretV2Body{\n\t\t\t\tChainID: ve.String(certItem.ChainID),\n\t\t\t}\n\t\t\tdescribeCertDetailSecretResp, err := c.sdkClient.DescribeCertDetailSecretV2(ctx, describeCertDetailSecretReq)\n\t\t\tc.logger.Debug(\"sdk request 'live.DescribeCertDetailSecretV2'\", slog.Any(\"request\", describeCertDetailSecretReq), slog.Any(\"response\", describeCertDetailSecretResp))\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 如果已存在相同证书，直接返回\n\t\t\toldCertPEM := strings.Join(describeCertDetailSecretResp.Result.SSL.Chain, \"\\n\\n\")\n\t\t\tif xcert.EqualCertificatesFromPEM(certPEM, oldCertPEM) {\n\t\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\t\treturn &certmgr.UploadResult{\n\t\t\t\t\tCertId:   certItem.ChainID,\n\t\t\t\t\tCertName: certItem.CertName,\n\t\t\t\t}, nil\n\t\t\t}\n\t\t}\n\t}\n\n\t// 生成新证书名（需符合火山引擎命名规则）\n\tcertName := fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())\n\n\t// 上传新证书\n\t// REF: https://www.volcengine.com/docs/6469/1186278#%E6%B7%BB%E5%8A%A0%E8%AF%81%E4%B9%A6\n\tcreateCertReq := &velive.CreateCertBody{\n\t\tCertName: ve.String(certName),\n\t\tRsa: velive.CreateCertBodyRsa{\n\t\t\tPrikey: privkeyPEM,\n\t\t\tPubkey: certPEM,\n\t\t},\n\t\tUseWay: \"https\",\n\t}\n\tcreateCertResp, err := c.sdkClient.CreateCert(ctx, createCertReq)\n\tc.logger.Debug(\"sdk request 'live.CreateCert'\", slog.Any(\"request\", createCertReq), slog.Any(\"response\", createCertResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'live.CreateCert': %w\", err)\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   *createCertResp.Result.ChainID,\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\treturn nil, certmgr.ErrUnsupported\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/wangsu-certificate/wangsu_certificate.go",
    "content": "package wangsucertificate\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\twangsusdk \"github.com/certimate-go/certimate/pkg/sdk3rd/wangsu/certificate\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype CertmgrConfig struct {\n\t// 网宿云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 网宿云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n}\n\ntype Certmgr struct {\n\tconfig    *CertmgrConfig\n\tlogger    *slog.Logger\n\tsdkClient *wangsusdk.Client\n}\n\nvar _ certmgr.Provider = (*Certmgr)(nil)\n\nfunc NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the certmgr provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Certmgr{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (c *Certmgr) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tc.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tc.logger = logger\n\t}\n}\n\nfunc (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 查询证书列表，避免重复上传\n\t// REF: https://www.wangsu.com/document/api-doc/22675?productCode=certificatemanagement\n\tlistCertificatesResp, err := c.sdkClient.ListCertificatesWithContext(ctx)\n\tc.logger.Debug(\"sdk request 'certificatemanagement.ListCertificates'\", slog.Any(\"response\", listCertificatesResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'certificatemanagement.ListCertificates': %w\", err)\n\t}\n\n\tif listCertificatesResp.Certificates != nil {\n\t\tfor _, certItem := range listCertificatesResp.Certificates {\n\t\t\t// 对比证书序列号\n\t\t\tif !strings.EqualFold(certX509.SerialNumber.Text(16), certItem.Serial) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 对比证书有效期\n\t\t\ttimezoneOfCST := time.FixedZone(\"CST\", 8*60*60)\n\t\t\toldCertNotBefore, _ := time.ParseInLocation(time.DateTime, certItem.ValidityFrom, timezoneOfCST)\n\t\t\toldCertNotAfter, _ := time.ParseInLocation(time.DateTime, certItem.ValidityTo, timezoneOfCST)\n\t\t\tif !certX509.NotBefore.Equal(oldCertNotBefore) || !certX509.NotAfter.Equal(oldCertNotAfter) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 如果以上信息都一致，则视为已存在相同证书，直接返回\n\t\t\tc.logger.Info(\"ssl certificate already exists\")\n\t\t\treturn &certmgr.UploadResult{\n\t\t\t\tCertId:   certItem.CertificateId,\n\t\t\t\tCertName: certItem.Name,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\t// 生成新证书名（需符合网宿云命名规则）\n\tcertName := fmt.Sprintf(\"certimate_%d\", time.Now().UnixMilli())\n\n\t// 新增证书\n\t// REF: https://www.wangsu.com/document/api-doc/25199?productCode=certificatemanagement\n\tcreateCertificateReq := &wangsusdk.CreateCertificateRequest{\n\t\tName:        lo.ToPtr(certName),\n\t\tCertificate: lo.ToPtr(certPEM),\n\t\tPrivateKey:  lo.ToPtr(privkeyPEM),\n\t\tComment:     lo.ToPtr(\"upload from certimate\"),\n\t}\n\tcreateCertificateResp, err := c.sdkClient.CreateCertificateWithContext(ctx, createCertificateReq)\n\tc.logger.Debug(\"sdk request 'certificatemanagement.CreateCertificate'\", slog.Any(\"request\", createCertificateReq), slog.Any(\"response\", createCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'certificatemanagement.CreateCertificate': %w\", err)\n\t}\n\n\t// 网宿云证书 URL 中包含证书 ID\n\t// 格式：\n\t//    https://open.chinanetcenter.com/api/certificate/100001\n\twangsuCertIdMatches := regexp.MustCompile(`/certificate/([0-9]+)`).FindStringSubmatch(createCertificateResp.CertificateLocation)\n\tif len(wangsuCertIdMatches) == 0 {\n\t\treturn nil, fmt.Errorf(\"received empty certificate id\")\n\t}\n\n\treturn &certmgr.UploadResult{\n\t\tCertId:   wangsuCertIdMatches[1],\n\t\tCertName: certName,\n\t}, nil\n}\n\nfunc (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) {\n\tcertId := certIdOrName\n\tcertName := fmt.Sprintf(\"certimate_%d\", time.Now().UnixMilli())\n\n\t// 修改证书\n\t// REF: https://www.wangsu.com/document/api-doc/25568?productCode=certificatemanagement\n\tupdateCertificateReq := &wangsusdk.UpdateCertificateRequest{\n\t\tName:        lo.ToPtr(certName),\n\t\tCertificate: lo.ToPtr(certPEM),\n\t\tPrivateKey:  lo.ToPtr(privkeyPEM),\n\t\tComment:     lo.ToPtr(\"upload from certimate\"),\n\t}\n\tupdateCertificateResp, err := c.sdkClient.UpdateCertificateWithContext(ctx, certId, updateCertificateReq)\n\tc.logger.Debug(\"sdk request 'certificatemanagement.UpdateCertificate'\", slog.Any(\"request\", updateCertificateReq), slog.Any(\"response\", updateCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'certificatemanagement.UpdateCertificate': %w\", err)\n\t}\n\n\treturn &certmgr.OperateResult{}, nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret string) (*wangsusdk.Client, error) {\n\treturn wangsusdk.NewClient(accessKeyId, accessKeySecret)\n}\n"
  },
  {
    "path": "pkg/core/certmgr/providers/wangsu-certificate/wangsu_certificate_test.go",
    "content": "package wangsucertificate_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/wangsu-certificate\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n)\n\nfunc init() {\n\targsPrefix := \"WANGSUCERTIFICATE_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./wangsu_certificate_test.go -args \\\n\t--WANGSUCERTIFICATE_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--WANGSUCERTIFICATE_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--WANGSUCERTIFICATE_ACCESSKEYID=\"your-access-key-id\" \\\n\t--WANGSUCERTIFICATE_ACCESSKEYSECRET=\"your-access-key-secret\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewCertmgr(&provider.CertmgrConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsres, _ := json.Marshal(res)\n\t\tt.Logf(\"ok: %s\", string(sres))\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/provider.go",
    "content": "package deployer\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n)\n\n// 表示定义 SSL 证书部署器的抽象类型接口。\ntype Provider interface {\n\t// 设置日志记录器。\n\t//\n\t// 入参：\n\t//   - logger：日志记录器实例。\n\tSetLogger(logger *slog.Logger)\n\n\t// 部署证书。\n\t//\n\t// 入参：\n\t//   - ctx：上下文。\n\t//   - certPEM：证书 PEM 内容。\n\t//   - privkeyPEM：私钥 PEM 内容。\n\t//\n\t// 出参：\n\t//   - res：部署结果。\n\t//   - err: 错误。\n\tDeploy(ctx context.Context, certPEM, privkeyPEM string) (_res *DeployResult, _err error)\n}\n\n// 表示 SSL 证书部署结果的数据结构。\ntype DeployResult struct {\n\tExtendedData map[string]any `json:\"extendedData,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/1panel/1panel.go",
    "content": "package onepanel\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/1panel\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tonepanelsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/1panel\"\n\tonepanelsdk2 \"github.com/certimate-go/certimate/pkg/sdk3rd/1panel/v2\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txwait \"github.com/certimate-go/certimate/pkg/utils/wait\"\n)\n\ntype DeployerConfig struct {\n\t// 1Panel 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// 1Panel 版本。\n\t// 可取值 \"v1\"、\"v2\"。\n\tApiVersion string `json:\"apiVersion\"`\n\t// 1Panel 接口密钥。\n\tApiKey string `json:\"apiKey\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 子节点名称。\n\t// 选填。\n\tNodeName string `json:\"nodeName,omitempty\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [WEBSITE_MATCH_PATTERN_SPECIFIED]。\n\tWebsiteMatchPattern string `json:\"websiteMatchPattern,omitempty\"`\n\t// 网站 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_WEBSITE]、且匹配模式非 [WEBSITE_MATCH_PATTERN_CERTSAN] 时必填。\n\tWebsiteId int64 `json:\"websiteId,omitempty\"`\n\t// 证书 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。\n\tCertificateId int64 `json:\"certificateId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  any\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiVersion, config.ApiKey, config.AllowInsecureConnections, config.NodeName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tServerUrl:                config.ServerUrl,\n\t\tApiVersion:               config.ApiVersion,\n\t\tApiKey:                   config.ApiKey,\n\t\tAllowInsecureConnections: config.AllowInsecureConnections,\n\t\tNodeName:                 config.NodeName,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_WEBSITE:\n\t\tif err := d.deployToWebsite(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_CERTIFICATE:\n\t\tif err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToWebsite(ctx context.Context, certPEM, privkeyPEM string) error {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的网站列表\n\tvar websiteIds []int64\n\tswitch d.config.WebsiteMatchPattern {\n\tcase \"\", WEBSITE_MATCH_PATTERN_SPECIFIED:\n\t\t{\n\t\t\tif d.config.WebsiteId == 0 {\n\t\t\t\treturn errors.New(\"config `websiteId` is required\")\n\t\t\t}\n\n\t\t\twebsiteIds = []int64{d.config.WebsiteId}\n\t\t}\n\n\tcase WEBSITE_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\twebsiteIdCandidates, err := d.getMatchedWebsiteIdsByCertificate(ctx, certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\twebsiteIds = websiteIdCandidates\n\t\t}\n\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported website match pattern: '%s'\", d.config.WebsiteMatchPattern)\n\t}\n\n\t// 遍历更新网站证书\n\tif len(websiteIds) == 0 {\n\t\td.logger.Info(\"no websites to deploy\")\n\t} else {\n\t\td.logger.Info(\"found websites to deploy\", slog.Any(\"websiteIds\", websiteIds))\n\t\tvar errs []error\n\n\t\twebsiteSSLId, _ := strconv.ParseInt(upres.CertId, 10, 64)\n\t\tfor i, websiteId := range websiteIds {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateWebsiteCertificate(ctx, websiteId, websiteSSLId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t\tif i < len(websiteIds)-1 {\n\t\t\t\t\txwait.DelayWithContext(ctx, time.Second*5)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.CertificateId == 0 {\n\t\treturn errors.New(\"config `certificateId` is required\")\n\t}\n\n\t// 替换证书\n\topres, err := d.sdkCertmgr.Replace(ctx, strconv.FormatInt(d.config.CertificateId, 10), certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to replace certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate replaced\", slog.Any(\"result\", opres))\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) getMatchedWebsiteIdsByCertificate(ctx context.Context, certPEM string) ([]int64, error) {\n\tvar websiteIds []int64\n\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch sdkClient := d.sdkClient.(type) {\n\tcase *onepanelsdk.Client:\n\t\t{\n\t\t\twebsiteSearchPage := 1\n\t\t\twebsiteSearchPageSize := 100\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn nil, ctx.Err()\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t\twebsiteSearchReq := &onepanelsdk.WebsiteSearchRequest{\n\t\t\t\t\tOrder:    \"ascending\",\n\t\t\t\t\tOrderBy:  \"primary_domain\",\n\t\t\t\t\tPage:     int32(websiteSearchPage),\n\t\t\t\t\tPageSize: int32(websiteSearchPageSize),\n\t\t\t\t}\n\t\t\t\twebsiteSearchResp, err := sdkClient.WebsiteSearchWithContext(ctx, websiteSearchReq)\n\t\t\t\td.logger.Debug(\"sdk request '1panel.WebsiteSearch'\", slog.Any(\"request\", websiteSearchReq), slog.Any(\"response\", websiteSearchResp))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request '1panel.WebsiteSearch': %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif websiteSearchResp.Data == nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tfor _, websiteItem := range websiteSearchResp.Data.Items {\n\t\t\t\t\tif certX509.VerifyHostname(websiteItem.PrimaryDomain) != nil {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\twebsiteGetResp, err := sdkClient.WebsiteGetWithContext(ctx, websiteItem.ID)\n\t\t\t\t\td.logger.Debug(\"sdk request '1panel.WebsiteGet'\", slog.Int64(\"websiteId\", websiteItem.ID), slog.Any(\"response\", websiteGetResp))\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request '1panel.WebsiteGet': %w\", err)\n\t\t\t\t\t}\n\n\t\t\t\t\tfor _, domainInfo := range websiteGetResp.Data.Domains {\n\t\t\t\t\t\tif domainInfo.SSL || certX509.VerifyHostname(domainInfo.Domain) == nil {\n\t\t\t\t\t\t\twebsiteIds = append(websiteIds, websiteItem.ID)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif len(websiteSearchResp.Data.Items) < websiteSearchPageSize {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\twebsiteSearchPage++\n\t\t\t}\n\t\t}\n\n\tcase *onepanelsdk2.Client:\n\t\t{\n\t\t\twebsiteSearchPage := 1\n\t\t\twebsiteSearchPageSize := 100\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn nil, ctx.Err()\n\t\t\t\tdefault:\n\t\t\t\t}\n\n\t\t\t\twebsiteSearchReq := &onepanelsdk2.WebsiteSearchRequest{\n\t\t\t\t\tOrder:    \"ascending\",\n\t\t\t\t\tOrderBy:  \"primary_domain\",\n\t\t\t\t\tPage:     int32(websiteSearchPage),\n\t\t\t\t\tPageSize: int32(websiteSearchPageSize),\n\t\t\t\t}\n\t\t\t\twebsiteSearchResp, err := sdkClient.WebsiteSearchWithContext(ctx, websiteSearchReq)\n\t\t\t\td.logger.Debug(\"sdk request '1panel.WebsiteSearch'\", slog.Any(\"request\", websiteSearchReq), slog.Any(\"response\", websiteSearchResp))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request '1panel.WebsiteSearch': %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif websiteSearchResp.Data == nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tfor _, websiteItem := range websiteSearchResp.Data.Items {\n\t\t\t\t\tif certX509.VerifyHostname(websiteItem.PrimaryDomain) != nil {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\twebsiteGetResp, err := sdkClient.WebsiteGetWithContext(ctx, websiteItem.ID)\n\t\t\t\t\td.logger.Debug(\"sdk request '1panel.WebsiteGet'\", slog.Int64(\"websiteId\", websiteItem.ID), slog.Any(\"response\", websiteGetResp))\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request '1panel.WebsiteGet': %w\", err)\n\t\t\t\t\t}\n\n\t\t\t\t\tfor _, domainInfo := range websiteGetResp.Data.Domains {\n\t\t\t\t\t\tif domainInfo.SSL || certX509.VerifyHostname(domainInfo.Domain) == nil {\n\t\t\t\t\t\t\twebsiteIds = append(websiteIds, websiteItem.ID)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif len(websiteSearchResp.Data.Items) < websiteSearchPageSize {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\twebsiteSearchPage++\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\tpanic(\"unreachable\")\n\t}\n\n\tif len(websiteIds) == 0 {\n\t\treturn nil, errors.New(\"could not find any websites matched by certificate\")\n\t}\n\n\treturn websiteIds, nil\n}\n\nfunc (d *Deployer) updateWebsiteCertificate(ctx context.Context, websiteId int64, websiteSSLId int64) error {\n\tswitch sdkClient := d.sdkClient.(type) {\n\tcase *onepanelsdk.Client:\n\t\t{\n\t\t\t// 获取网站 HTTPS 配置\n\t\t\twebsiteHttpsGetResp, err := sdkClient.WebsiteHttpsGetWithContext(ctx, websiteId)\n\t\t\td.logger.Debug(\"sdk request '1panel.WebsiteHttpsGet'\", slog.Int64(\"websiteId\", websiteId), slog.Any(\"response\", websiteHttpsGetResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request '1panel.WebsiteHttpsGet': %w\", err)\n\t\t\t} else {\n\t\t\t\tif websiteHttpsGetResp.Data.Enable && websiteHttpsGetResp.Data.WebsiteSSLID == websiteSSLId {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 修改网站 HTTPS 配置\n\t\t\twebsiteHttpsPostReq := &onepanelsdk.WebsiteHttpsPostRequest{\n\t\t\t\tWebsiteID:    websiteId,\n\t\t\t\tType:         \"existed\",\n\t\t\t\tWebsiteSSLID: websiteSSLId,\n\t\t\t\tEnable:       true,\n\t\t\t\tHttpConfig:   websiteHttpsGetResp.Data.HttpConfig,\n\t\t\t\tSSLProtocol:  websiteHttpsGetResp.Data.SSLProtocol,\n\t\t\t\tAlgorithm:    websiteHttpsGetResp.Data.Algorithm,\n\t\t\t\tHsts:         websiteHttpsGetResp.Data.Hsts,\n\t\t\t}\n\t\t\tif websiteHttpsPostReq.HttpConfig == \"\" {\n\t\t\t\twebsiteHttpsPostReq.HttpConfig = \"HTTPToHTTPS\"\n\t\t\t}\n\t\t\twebsiteHttpsPostResp, err := sdkClient.WebsiteHttpsPostWithContext(ctx, websiteId, websiteHttpsPostReq)\n\t\t\td.logger.Debug(\"sdk request '1panel.WebsiteHttpsPost'\", slog.Int64(\"websiteId\", websiteId), slog.Any(\"request\", websiteHttpsPostReq), slog.Any(\"response\", websiteHttpsPostResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request '1panel.WebsiteHttpsPost': %w\", err)\n\t\t\t}\n\t\t}\n\n\tcase *onepanelsdk2.Client:\n\t\t{\n\t\t\t// 获取网站 HTTPS 配置\n\t\t\twebsiteHttpsGetResp, err := sdkClient.WebsiteHttpsGetWithContext(ctx, websiteId)\n\t\t\td.logger.Debug(\"sdk request '1panel.WebsiteHttpsGet'\", slog.Int64(\"websiteId\", websiteId), slog.Any(\"response\", websiteHttpsGetResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request '1panel.WebsiteHttpsGet': %w\", err)\n\t\t\t} else {\n\t\t\t\tif websiteHttpsGetResp.Data.Enable && websiteHttpsGetResp.Data.WebsiteSSLID == websiteSSLId {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 修改网站 HTTPS 配置\n\t\t\twebsiteHttpsPostReq := &onepanelsdk2.WebsiteHttpsPostRequest{\n\t\t\t\tWebsiteID:    websiteId,\n\t\t\t\tType:         \"existed\",\n\t\t\t\tWebsiteSSLID: websiteSSLId,\n\t\t\t\tEnable:       true,\n\t\t\t\tHttpConfig:   websiteHttpsGetResp.Data.HttpConfig,\n\t\t\t\tSSLProtocol:  websiteHttpsGetResp.Data.SSLProtocol,\n\t\t\t\tAlgorithm:    websiteHttpsGetResp.Data.Algorithm,\n\t\t\t\tHsts:         websiteHttpsGetResp.Data.Hsts,\n\t\t\t\tHttp3:        websiteHttpsGetResp.Data.Http3,\n\t\t\t}\n\t\t\tif websiteHttpsPostReq.HttpConfig == \"\" {\n\t\t\t\twebsiteHttpsPostReq.HttpConfig = \"HTTPToHTTPS\"\n\t\t\t}\n\t\t\twebsiteHttpsPostResp, err := sdkClient.WebsiteHttpsPostWithContext(ctx, websiteId, websiteHttpsPostReq)\n\t\t\td.logger.Debug(\"sdk request '1panel.WebsiteHttpsPost'\", slog.Int64(\"websiteId\", websiteId), slog.Any(\"request\", websiteHttpsPostReq), slog.Any(\"response\", websiteHttpsPostResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request '1panel.WebsiteHttpsPost': %w\", err)\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\tpanic(\"unreachable\")\n\t}\n\n\treturn nil\n}\n\nconst (\n\tsdkVersionV1 = \"v1\"\n\tsdkVersionV2 = \"v2\"\n)\n\nfunc createSDKClient(serverUrl, apiVersion, apiKey string, skipTlsVerify bool, nodeName string) (any, error) {\n\tif apiVersion == sdkVersionV1 {\n\t\tclient, err := onepanelsdk.NewClient(serverUrl, apiKey)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif skipTlsVerify {\n\t\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t\t}\n\n\t\treturn client, nil\n\t} else if apiVersion == sdkVersionV2 {\n\t\tvar client *onepanelsdk2.Client\n\t\tvar err error\n\n\t\tif nodeName == \"\" {\n\t\t\tclient, err = onepanelsdk2.NewClient(serverUrl, apiKey)\n\t\t} else {\n\t\t\tclient, err = onepanelsdk2.NewClientWithNode(serverUrl, apiKey, nodeName)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif skipTlsVerify {\n\t\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t\t}\n\n\t\treturn client, nil\n\t}\n\n\treturn nil, errors.New(\"1panel: invalid api version\")\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/1panel/1panel_test.go",
    "content": "package onepanel_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/1panel\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfApiVersion    string\n\tfApiKey        string\n\tfWebsiteId     int64\n)\n\nfunc init() {\n\targsPrefix := \"1PANEL_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiVersion, argsPrefix+\"APIVERSION\", \"v1\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n\tflag.Int64Var(&fWebsiteId, argsPrefix+\"WEBSITEID\", 0, \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./1panel_test.go -args \\\n\t--1PANEL_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--1PANEL_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--1PANEL_SERVERURL=\"http://127.0.0.1:20410\" \\\n\t--1PANEL_APIVERSION=\"v1\" \\\n\t--1PANEL_APIKEY=\"your-api-key\" \\\n\t--1PANEL_WEBSITEID=\"your-website-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APIVERSION: %v\", fApiVersion),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t\tfmt.Sprintf(\"WEBSITEID: %v\", fWebsiteId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiVersion:               fApiVersion,\n\t\t\tApiKey:                   fApiKey,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tResourceType:             provider.RESOURCE_TYPE_WEBSITE,\n\t\t\tWebsiteMatchPattern:      provider.WEBSITE_MATCH_PATTERN_SPECIFIED,\n\t\t\tWebsiteId:                fWebsiteId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/1panel/consts.go",
    "content": "package onepanel\n\nconst (\n\t// 资源类型：替换指定网站的证书。\n\tRESOURCE_TYPE_WEBSITE = \"website\"\n\t// 资源类型：替换指定证书。\n\tRESOURCE_TYPE_CERTIFICATE = \"certificate\"\n)\n\nconst (\n\t// 匹配模式：指定 ID。\n\tWEBSITE_MATCH_PATTERN_SPECIFIED = \"specified\"\n\t// 匹配模式：证书 SAN 匹配。\n\tWEBSITE_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/1panel-console/1panel_console.go",
    "content": "package onepanelconsole\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tonepanelsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/1panel\"\n\tonepanelsdk2 \"github.com/certimate-go/certimate/pkg/sdk3rd/1panel/v2\"\n)\n\ntype DeployerConfig struct {\n\t// 1Panel 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// 1Panel 版本。\n\t// 可取值 \"v1\"、\"v2\"。\n\tApiVersion string `json:\"apiVersion\"`\n\t// 1Panel 接口密钥。\n\tApiKey string `json:\"apiKey\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 是否自动重启。\n\tAutoRestart bool `json:\"autoRestart\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient any\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiVersion, config.ApiKey, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 设置面板 SSL 证书\n\tswitch sdkClient := d.sdkClient.(type) {\n\tcase *onepanelsdk.Client:\n\t\t{\n\t\t\tsettingsSSLUpdateReq := &onepanelsdk.SettingsSSLUpdateRequest{\n\t\t\t\tCert:        certPEM,\n\t\t\t\tKey:         privkeyPEM,\n\t\t\t\tSSL:         \"enable\",\n\t\t\t\tSSLType:     \"import-paste\",\n\t\t\t\tAutoRestart: strconv.FormatBool(d.config.AutoRestart),\n\t\t\t}\n\t\t\tsettingsSSLUpdateResp, err := sdkClient.SettingsSSLUpdateWithContext(ctx, settingsSSLUpdateReq)\n\t\t\td.logger.Debug(\"sdk request '1panel.SettingsSSLUpdate'\", slog.Any(\"request\", settingsSSLUpdateReq), slog.Any(\"response\", settingsSSLUpdateResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request '1panel.SettingsSSLUpdate': %w\", err)\n\t\t\t}\n\t\t}\n\n\tcase *onepanelsdk2.Client:\n\t\t{\n\t\t\tcoreSettingsSSLUpdateReq := &onepanelsdk2.CoreSettingsSSLUpdateRequest{\n\t\t\t\tCert:        certPEM,\n\t\t\t\tKey:         privkeyPEM,\n\t\t\t\tSSL:         \"Enable\",\n\t\t\t\tSSLType:     \"import-paste\",\n\t\t\t\tAutoRestart: strconv.FormatBool(d.config.AutoRestart),\n\t\t\t}\n\t\t\tcoreSettingsSSLUpdateResp, err := sdkClient.CoreSettingsSSLUpdateWithContext(ctx, coreSettingsSSLUpdateReq)\n\t\t\td.logger.Debug(\"sdk request '1panel.CoreSettingsSSLUpdate'\", slog.Any(\"request\", coreSettingsSSLUpdateReq), slog.Any(\"response\", coreSettingsSSLUpdateResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request '1panel.CoreSettingsSSLUpdate': %w\", err)\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\tpanic(\"unreachable\")\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nconst (\n\tsdkVersionV1 = \"v1\"\n\tsdkVersionV2 = \"v2\"\n)\n\nfunc createSDKClient(serverUrl, apiVersion, apiKey string, skipTlsVerify bool) (any, error) {\n\tif apiVersion == sdkVersionV1 {\n\t\tclient, err := onepanelsdk.NewClient(serverUrl, apiKey)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif skipTlsVerify {\n\t\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t\t}\n\n\t\treturn client, nil\n\t} else if apiVersion == sdkVersionV2 {\n\t\tclient, err := onepanelsdk2.NewClient(serverUrl, apiKey)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif skipTlsVerify {\n\t\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t\t}\n\n\t\treturn client, nil\n\t}\n\n\treturn nil, errors.New(\"1panel: invalid api version\")\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/1panel-console/1panel_console_test.go",
    "content": "package onepanelconsole_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/1panel-console\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfApiVersion    string\n\tfApiKey        string\n)\n\nfunc init() {\n\targsPrefix := \"1PANELCONSOLE_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiVersion, argsPrefix+\"APIVERSION\", \"v1\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./1panel_console_test.go -args \\\n\t--1PANELCONSOLE_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--1PANELCONSOLE_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--1PANELCONSOLE_SERVERURL=\"http://127.0.0.1:20410\" \\\n\t--1PANELCONSOLE_APIVERSION=\"v1\" \\\n\t--1PANELCONSOLE_APIKEY=\"your-api-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APIVERSION: %v\", fApiVersion),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiVersion:               fApiVersion,\n\t\t\tApiKey:                   fApiKey,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tAutoRestart:              true,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go",
    "content": "package aliyunalb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\talialb \"github.com/alibabacloud-go/alb-20200616/v2/client\"\n\talicas \"github.com/alibabacloud-go/cas-20200407/v4/client\"\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-alb/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txwait \"github.com/certimate-go/certimate/pkg/utils/wait\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 负载均衡实例 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。\n\tLoadbalancerId string `json:\"loadbalancerId,omitempty\"`\n\t// 负载均衡监听 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。\n\tListenerId string `json:\"listenerId,omitempty\"`\n\t// SNI 域名（支持泛域名）。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。\n\tDomain string `json:\"domain,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClients *wSDKClients\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\ntype wSDKClients struct {\n\tALB *internal.AlbClient\n\tCAS *internal.CasClient\n}\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclients, err := createSDKClients(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tResourceGroupId: config.ResourceGroupId,\n\t\tRegion: lo.\n\t\t\tIf(config.Region == \"\" || strings.HasPrefix(config.Region, \"cn-\"), \"cn-hangzhou\").\n\t\t\tElse(\"ap-southeast-1\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClients: clients,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_LOADBALANCER:\n\t\tif err := d.deployToLoadbalancer(ctx, upres.ExtendedData[\"CertIdentifier\"].(string), certX509.DNSNames); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_LISTENER:\n\t\tif err := d.deployToListener(ctx, upres.ExtendedData[\"CertIdentifier\"].(string), certX509.DNSNames); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string, cloudCertSANs []string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\n\t// 查询负载均衡实例的详细信息\n\t// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getloadbalancerattribute\n\tgetLoadBalancerAttributeReq := &alialb.GetLoadBalancerAttributeRequest{\n\t\tLoadBalancerId: tea.String(d.config.LoadbalancerId),\n\t}\n\tgetLoadBalancerAttributeResp, err := d.sdkClients.ALB.GetLoadBalancerAttributeWithContext(ctx, getLoadBalancerAttributeReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'alb.GetLoadBalancerAttribute'\", slog.Any(\"request\", getLoadBalancerAttributeReq), slog.Any(\"response\", getLoadBalancerAttributeResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'alb.GetLoadBalancerAttribute': %w\", err)\n\t}\n\n\t// 查询 HTTPS 监听列表\n\t// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners\n\tlistenerIds := make([]string, 0)\n\tlistListenersToken := (*string)(nil)\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistListenersReq := &alialb.ListListenersRequest{\n\t\t\tNextToken:        listListenersToken,\n\t\t\tMaxResults:       tea.Int32(100),\n\t\t\tLoadBalancerIds:  tea.StringSlice([]string{d.config.LoadbalancerId}),\n\t\t\tListenerProtocol: tea.String(\"HTTPS\"),\n\t\t}\n\t\tlistListenersResp, err := d.sdkClients.ALB.ListListenersWithContext(ctx, listListenersReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'alb.ListListeners'\", slog.Any(\"request\", listListenersReq), slog.Any(\"response\", listListenersResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'alb.ListListeners': %w\", err)\n\t\t}\n\n\t\tif listListenersResp.Body == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, listener := range listListenersResp.Body.Listeners {\n\t\t\tlistenerIds = append(listenerIds, tea.StringValue(listener.ListenerId))\n\t\t}\n\n\t\tif len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tlistListenersToken = listListenersResp.Body.NextToken\n\t}\n\n\t// 查询 QUIC 监听列表\n\t// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners\n\tlistListenersToken = nil\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistListenersReq := &alialb.ListListenersRequest{\n\t\t\tNextToken:        listListenersToken,\n\t\t\tMaxResults:       tea.Int32(100),\n\t\t\tLoadBalancerIds:  tea.StringSlice([]string{d.config.LoadbalancerId}),\n\t\t\tListenerProtocol: tea.String(\"QUIC\"),\n\t\t}\n\t\tlistListenersResp, err := d.sdkClients.ALB.ListListenersWithContext(ctx, listListenersReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'alb.ListListeners'\", slog.Any(\"request\", listListenersReq), slog.Any(\"response\", listListenersResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'alb.ListListeners': %w\", err)\n\t\t}\n\n\t\tif listListenersResp.Body == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, listener := range listListenersResp.Body.Listeners {\n\t\t\tlistenerIds = append(listenerIds, tea.StringValue(listener.ListenerId))\n\t\t}\n\n\t\tif len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tlistListenersToken = listListenersResp.Body.NextToken\n\t}\n\n\t// 遍历更新监听证书\n\tif len(listenerIds) == 0 {\n\t\td.logger.Info(\"no alb listeners to deploy\")\n\t} else {\n\t\tvar errs []error\n\t\td.logger.Info(\"found https/quic listeners to deploy\", slog.Any(\"listenerIds\", listenerIds))\n\n\t\tfor _, listenerId := range listenerIds {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateListenerCertificate(ctx, listenerId, cloudCertId, cloudCertSANs); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToListener(ctx context.Context, cloudCertId string, cloudCertSANs []string) error {\n\tif d.config.ListenerId == \"\" {\n\t\treturn errors.New(\"config `listenerId` is required\")\n\t}\n\n\t// 更新监听\n\tif err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId, cloudCertSANs); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string, cloudCertSANs []string) error {\n\tif d.config.Domain == \"\" {\n\t\t// 未指定 SNI，只需部署到监听器\n\n\t\tif err := d.waitForListenerReady(ctx, cloudListenerId); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// 修改监听的属性\n\t\t// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-updatelistenerattribute\n\t\tupdateListenerAttributeReq := &alialb.UpdateListenerAttributeRequest{\n\t\t\tListenerId: tea.String(cloudListenerId),\n\t\t\tCertificates: []*alialb.UpdateListenerAttributeRequestCertificates{{\n\t\t\t\tCertificateId: tea.String(cloudCertId),\n\t\t\t}},\n\t\t}\n\t\tupdateListenerAttributeResp, err := d.sdkClients.ALB.UpdateListenerAttributeWithContext(ctx, updateListenerAttributeReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'alb.UpdateListenerAttribute'\", slog.Any(\"request\", updateListenerAttributeReq), slog.Any(\"response\", updateListenerAttributeResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'alb.UpdateListenerAttribute': %w\", err)\n\t\t}\n\t} else {\n\t\t// 指定 SNI，需部署到扩展域名\n\n\t\t// 查询监听证书列表\n\t\t// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlistenercertificates\n\t\tlistenerCertificates := make([]alialb.ListListenerCertificatesResponseBodyCertificates, 0)\n\t\tlistListenerCertificatesToken := (*string)(nil)\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\tlistListenerCertificatesReq := &alialb.ListListenerCertificatesRequest{\n\t\t\t\tNextToken:       listListenerCertificatesToken,\n\t\t\t\tMaxResults:      tea.Int32(100),\n\t\t\t\tListenerId:      tea.String(cloudListenerId),\n\t\t\t\tCertificateType: tea.String(\"Server\"),\n\t\t\t}\n\t\t\tlistListenerCertificatesResp, err := d.sdkClients.ALB.ListListenerCertificatesWithContext(ctx, listListenerCertificatesReq, &dara.RuntimeOptions{})\n\t\t\td.logger.Debug(\"sdk request 'alb.ListListenerCertificates'\", slog.Any(\"request\", listListenerCertificatesReq), slog.Any(\"response\", listListenerCertificatesResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'alb.ListListenerCertificates': %w\", err)\n\t\t\t}\n\n\t\t\tif listListenerCertificatesResp.Body == nil {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tfor _, listenerCertificate := range listListenerCertificatesResp.Body.Certificates {\n\t\t\t\tlistenerCertificates = append(listenerCertificates, *listenerCertificate)\n\t\t\t}\n\n\t\t\tif len(listListenerCertificatesResp.Body.Certificates) == 0 || listListenerCertificatesResp.Body.NextToken == nil {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tlistListenerCertificatesToken = listListenerCertificatesResp.Body.NextToken\n\t\t}\n\n\t\t// 查询监听证书，并找出需要解除关联的证书\n\t\t// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlistenercertificates\n\t\t// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-getcertificatedetail\n\t\tcertificateIsAlreadyAssociated := false\n\t\tcertificateIdsToDissociate := make([]string, 0)\n\t\tif len(listenerCertificates) > 0 {\n\t\t\td.logger.Info(\"found listener certificates to deploy\", slog.Any(\"listenerCertificates\", listenerCertificates))\n\t\t\tvar errs []error\n\n\t\t\tfor _, listenerCertificate := range listenerCertificates {\n\t\t\t\tif tea.BoolValue(listenerCertificate.IsDefault) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif !strings.EqualFold(tea.StringValue(listenerCertificate.Status), \"Associated\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif tea.StringValue(listenerCertificate.CertificateId) == cloudCertId {\n\t\t\t\t\tcertificateIsAlreadyAssociated = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tcertificateId := strings.SplitN(tea.StringValue(listenerCertificate.CertificateId), \"-\", 2)[0]\n\t\t\t\tcertificateIdAsInt64, err := strconv.ParseInt(certificateId, 10, 64)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tgetCertificateDetailReq := &alicas.GetCertificateDetailRequest{\n\t\t\t\t\tCertificateId: tea.Int64(certificateIdAsInt64),\n\t\t\t\t}\n\t\t\t\tgetCertificateDetailResp, err := d.sdkClients.CAS.GetCertificateDetailWithContext(ctx, getCertificateDetailReq, &dara.RuntimeOptions{})\n\t\t\t\td.logger.Debug(\"sdk request 'cas.GetCertificateDetail'\", slog.Any(\"request\", getCertificateDetailReq), slog.Any(\"response\", getCertificateDetailResp))\n\t\t\t\tif err != nil {\n\t\t\t\t\tif sdkerr, ok := err.(*tea.SDKError); ok {\n\t\t\t\t\t\tif tea.IntValue(sdkerr.StatusCode) == 400 && tea.StringValue(sdkerr.Code) == \"NotFound\" {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\terrs = append(errs, fmt.Errorf(\"failed to execute sdk request 'cas.GetCertificateDetail': %w\", err))\n\t\t\t\t\tcontinue\n\t\t\t\t} else {\n\t\t\t\t\tcertCNMatched := tea.StringValue(getCertificateDetailResp.Body.CommonName) == d.config.Domain\n\t\t\t\t\tcertSANDiff, _ := lo.Difference(tea.StringSliceValue(getCertificateDetailResp.Body.SubjectAlternativeNames), cloudCertSANs)\n\t\t\t\t\tif certCNMatched || len(certSANDiff) == 0 {\n\t\t\t\t\t\tcertificateIdsToDissociate = append(certificateIdsToDissociate, certificateId)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tcertNotAfter := time.Unix(tea.Int64Value(getCertificateDetailResp.Body.NotAfter)/1000, 0)\n\t\t\t\t\tif certNotAfter.Before(time.Now()) {\n\t\t\t\t\t\tcertificateIdsToDissociate = append(certificateIdsToDissociate, certificateId)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(errs) > 0 {\n\t\t\t\treturn errors.Join(errs...)\n\t\t\t}\n\t\t}\n\n\t\t// 关联监听和扩展证书\n\t\t// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-associateadditionalcertificateswithlistener\n\t\tif !certificateIsAlreadyAssociated {\n\t\t\tif err := d.waitForListenerReady(ctx, cloudListenerId); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tassociateAdditionalCertificatesFromListenerReq := &alialb.AssociateAdditionalCertificatesWithListenerRequest{\n\t\t\t\tListenerId: tea.String(cloudListenerId),\n\t\t\t\tCertificates: []*alialb.AssociateAdditionalCertificatesWithListenerRequestCertificates{\n\t\t\t\t\t{\n\t\t\t\t\t\tCertificateId: tea.String(cloudCertId),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tassociateAdditionalCertificatesFromListenerResp, err := d.sdkClients.ALB.AssociateAdditionalCertificatesWithListenerWithContext(ctx, associateAdditionalCertificatesFromListenerReq, &dara.RuntimeOptions{})\n\t\t\td.logger.Debug(\"sdk request 'alb.AssociateAdditionalCertificatesWithListener'\", slog.Any(\"request\", associateAdditionalCertificatesFromListenerReq), slog.Any(\"response\", associateAdditionalCertificatesFromListenerResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'alb.AssociateAdditionalCertificatesWithListener': %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// 解除关联监听和扩展证书\n\t\t// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-dissociateadditionalcertificatesfromlistener\n\t\tif !certificateIsAlreadyAssociated && len(certificateIdsToDissociate) > 0 {\n\t\t\tif err := d.waitForListenerReady(ctx, cloudListenerId); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tdissociateAdditionalCertificatesFromListenerReq := &alialb.DissociateAdditionalCertificatesFromListenerRequest{\n\t\t\t\tListenerId: tea.String(cloudListenerId),\n\t\t\t\tCertificates: lo.Map(certificateIdsToDissociate, func(certificateId string, _ int) *alialb.DissociateAdditionalCertificatesFromListenerRequestCertificates {\n\t\t\t\t\treturn &alialb.DissociateAdditionalCertificatesFromListenerRequestCertificates{\n\t\t\t\t\t\tCertificateId: tea.String(certificateId),\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\t}\n\t\t\tdissociateAdditionalCertificatesFromListenerResp, err := d.sdkClients.ALB.DissociateAdditionalCertificatesFromListenerWithContext(ctx, dissociateAdditionalCertificatesFromListenerReq, &dara.RuntimeOptions{})\n\t\t\td.logger.Debug(\"sdk request 'alb.DissociateAdditionalCertificatesFromListener'\", slog.Any(\"request\", dissociateAdditionalCertificatesFromListenerReq), slog.Any(\"response\", dissociateAdditionalCertificatesFromListenerResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'alb.DissociateAdditionalCertificatesFromListener': %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) waitForListenerReady(ctx context.Context, cloudListenerId string) error {\n\t// 查询监听的属性，直到监听状态不再为 \"Configuring\"\n\t// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getlistenerattribute\n\tif _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) {\n\t\tgetListenerAttributeReq := &alialb.GetListenerAttributeRequest{\n\t\t\tListenerId: tea.String(cloudListenerId),\n\t\t}\n\t\tgetListenerAttributeResp, err := d.sdkClients.ALB.GetListenerAttributeWithContext(ctx, getListenerAttributeReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'alb.GetListenerAttribute'\", slog.Any(\"request\", getListenerAttributeReq), slog.Any(\"response\", getListenerAttributeResp))\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to execute sdk request 'alb.GetListenerAttribute': %w\", err)\n\t\t}\n\n\t\tif tea.StringValue(getListenerAttributeResp.Body.ListenerStatus) != \"Configuring\" {\n\t\t\treturn true, nil\n\t\t}\n\n\t\td.logger.Info(\"waiting for aliyun alb listener's status to not be 'Configuring' ...\")\n\t\treturn false, nil\n\t}, time.Second*5); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClients(accessKeyId, accessKeySecret, region string) (*wSDKClients, error) {\n\t// 接入点一览 https://api.aliyun.com/product/Alb\n\tvar albEndpoint string\n\tswitch region {\n\tcase \"\", \"cn-hangzhou-finance\":\n\t\talbEndpoint = \"alb.cn-hangzhou.aliyuncs.com\"\n\tdefault:\n\t\talbEndpoint = fmt.Sprintf(\"alb.%s.aliyuncs.com\", region)\n\t}\n\n\talbConfig := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(albEndpoint),\n\t}\n\talbClient, err := internal.NewAlbClient(albConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 接入点一览 https://api.aliyun.com/product/cas\n\tvar casEndpoint string\n\tif !strings.HasPrefix(region, \"cn-\") {\n\t\tcasEndpoint = \"cas.ap-southeast-1.aliyuncs.com\"\n\t} else {\n\t\tcasEndpoint = \"cas.aliyuncs.com\"\n\t}\n\n\tcasConfig := &aliopen.Config{\n\t\tEndpoint:        tea.String(casEndpoint),\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t}\n\tcasClient, err := internal.NewCasClient(casConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &wSDKClients{\n\t\tALB: albClient,\n\t\tCAS: casClient,\n\t}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-alb/aliyun_alb_test.go",
    "content": "package aliyunalb_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-alb\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfLoadbalancerId  string\n\tfListenerId      string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNALB_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fLoadbalancerId, argsPrefix+\"LOADBALANCERID\", \"\", \"\")\n\tflag.StringVar(&fListenerId, argsPrefix+\"LISTENERID\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_alb_test.go -args \\\n\t--ALIYUNALB_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNALB_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNALB_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNALB_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNALB_REGION=\"cn-hangzhou\" \\\n\t--ALIYUNALB_LOADBALANCERID=\"your-alb-instance-id\" \\\n\t--ALIYUNALB_LISTENERID=\"your-alb-listener-id\" \\\n\t--ALIYUNALB_DOMAIN=\"your-alb-sni-domain\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy_ToLoadbalancer\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LOADBALANCERID: %v\", fLoadbalancerId),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegion:          fRegion,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LOADBALANCER,\n\t\t\tLoadbalancerId:  fLoadbalancerId,\n\t\t\tDomain:          fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n\n\tt.Run(\"Deploy_ToListener\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LISTENERID: %v\", fListenerId),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegion:          fRegion,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LISTENER,\n\t\t\tListenerId:      fListenerId,\n\t\t\tDomain:          fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-alb/consts.go",
    "content": "package aliyunalb\n\nconst (\n\t// 资源类型：部署到指定负载均衡器。\n\tRESOURCE_TYPE_LOADBALANCER = \"loadbalancer\"\n\t// 资源类型：部署到指定监听器。\n\tRESOURCE_TYPE_LISTENER = \"listener\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-alb/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\n\talialb \"github.com/alibabacloud-go/alb-20200616/v2/client\"\n\talicas \"github.com/alibabacloud-go/cas-20200407/v4/client\"\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\topenapiutil \"github.com/alibabacloud-go/darabonba-openapi/v2/utils\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/cas-20200407/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype CasClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewCasClient(config *openapiutil.Config) (*CasClient, error) {\n\tclient := new(CasClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *CasClient) Init(config *openapiutil.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *CasClient) GetCertificateDetailWithContext(ctx context.Context, request *alicas.GetCertificateDetailRequest, runtime *dara.RuntimeOptions) (_result *alicas.GetCertificateDetailResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\n\tquery := map[string]interface{}{}\n\tif !dara.IsNil(request.CertificateId) {\n\t\tquery[\"CertificateId\"] = request.CertificateId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"GetCertificateDetail\"),\n\t\tVersion:     dara.String(\"2020-04-07\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alicas.GetCertificateDetailResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\n// This is a partial copy of https://github.com/alibabacloud-go/alb-20200616/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype AlbClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewAlbClient(config *openapiutil.Config) (*AlbClient, error) {\n\tclient := new(AlbClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *AlbClient) Init(config *openapiutil.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *AlbClient) AssociateAdditionalCertificatesWithListenerWithContext(ctx context.Context, request *alialb.AssociateAdditionalCertificatesWithListenerRequest, runtime *dara.RuntimeOptions) (_result *alialb.AssociateAdditionalCertificatesWithListenerResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.Certificates) {\n\t\tquery[\"Certificates\"] = request.Certificates\n\t}\n\n\tif !dara.IsNil(request.ClientToken) {\n\t\tquery[\"ClientToken\"] = request.ClientToken\n\t}\n\n\tif !dara.IsNil(request.DryRun) {\n\t\tquery[\"DryRun\"] = request.DryRun\n\t}\n\n\tif !dara.IsNil(request.ListenerId) {\n\t\tquery[\"ListenerId\"] = request.ListenerId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"AssociateAdditionalCertificatesWithListener\"),\n\t\tVersion:     dara.String(\"2020-06-16\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alialb.AssociateAdditionalCertificatesWithListenerResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *AlbClient) DissociateAdditionalCertificatesFromListenerWithContext(ctx context.Context, request *alialb.DissociateAdditionalCertificatesFromListenerRequest, runtime *dara.RuntimeOptions) (_result *alialb.DissociateAdditionalCertificatesFromListenerResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.Certificates) {\n\t\tquery[\"Certificates\"] = request.Certificates\n\t}\n\n\tif !dara.IsNil(request.ClientToken) {\n\t\tquery[\"ClientToken\"] = request.ClientToken\n\t}\n\n\tif !dara.IsNil(request.DryRun) {\n\t\tquery[\"DryRun\"] = request.DryRun\n\t}\n\n\tif !dara.IsNil(request.ListenerId) {\n\t\tquery[\"ListenerId\"] = request.ListenerId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DissociateAdditionalCertificatesFromListener\"),\n\t\tVersion:     dara.String(\"2020-06-16\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alialb.DissociateAdditionalCertificatesFromListenerResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *AlbClient) GetListenerAttributeWithContext(ctx context.Context, request *alialb.GetListenerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alialb.GetListenerAttributeResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.ListenerId) {\n\t\tquery[\"ListenerId\"] = request.ListenerId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"GetListenerAttribute\"),\n\t\tVersion:     dara.String(\"2020-06-16\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alialb.GetListenerAttributeResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *AlbClient) GetLoadBalancerAttributeWithContext(ctx context.Context, request *alialb.GetLoadBalancerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alialb.GetLoadBalancerAttributeResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.LoadBalancerId) {\n\t\tquery[\"LoadBalancerId\"] = request.LoadBalancerId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"GetLoadBalancerAttribute\"),\n\t\tVersion:     dara.String(\"2020-06-16\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alialb.GetLoadBalancerAttributeResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *AlbClient) ListListenerCertificatesWithContext(ctx context.Context, request *alialb.ListListenerCertificatesRequest, runtime *dara.RuntimeOptions) (_result *alialb.ListListenerCertificatesResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.CertificateIds) {\n\t\tquery[\"CertificateIds\"] = request.CertificateIds\n\t}\n\n\tif !dara.IsNil(request.CertificateType) {\n\t\tquery[\"CertificateType\"] = request.CertificateType\n\t}\n\n\tif !dara.IsNil(request.ListenerId) {\n\t\tquery[\"ListenerId\"] = request.ListenerId\n\t}\n\n\tif !dara.IsNil(request.MaxResults) {\n\t\tquery[\"MaxResults\"] = request.MaxResults\n\t}\n\n\tif !dara.IsNil(request.NextToken) {\n\t\tquery[\"NextToken\"] = request.NextToken\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"ListListenerCertificates\"),\n\t\tVersion:     dara.String(\"2020-06-16\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alialb.ListListenerCertificatesResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *AlbClient) ListListenersWithContext(ctx context.Context, request *alialb.ListListenersRequest, runtime *dara.RuntimeOptions) (_result *alialb.ListListenersResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.ListenerIds) {\n\t\tquery[\"ListenerIds\"] = request.ListenerIds\n\t}\n\n\tif !dara.IsNil(request.ListenerProtocol) {\n\t\tquery[\"ListenerProtocol\"] = request.ListenerProtocol\n\t}\n\n\tif !dara.IsNil(request.LoadBalancerIds) {\n\t\tquery[\"LoadBalancerIds\"] = request.LoadBalancerIds\n\t}\n\n\tif !dara.IsNil(request.MaxResults) {\n\t\tquery[\"MaxResults\"] = request.MaxResults\n\t}\n\n\tif !dara.IsNil(request.NextToken) {\n\t\tquery[\"NextToken\"] = request.NextToken\n\t}\n\n\tif !dara.IsNil(request.Tag) {\n\t\tquery[\"Tag\"] = request.Tag\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"ListListeners\"),\n\t\tVersion:     dara.String(\"2020-06-16\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alialb.ListListenersResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *AlbClient) UpdateListenerAttributeWithContext(ctx context.Context, request *alialb.UpdateListenerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alialb.UpdateListenerAttributeResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.CaCertificates) {\n\t\tquery[\"CaCertificates\"] = request.CaCertificates\n\t}\n\n\tif !dara.IsNil(request.CaEnabled) {\n\t\tquery[\"CaEnabled\"] = request.CaEnabled\n\t}\n\n\tif !dara.IsNil(request.Certificates) {\n\t\tquery[\"Certificates\"] = request.Certificates\n\t}\n\n\tif !dara.IsNil(request.ClientToken) {\n\t\tquery[\"ClientToken\"] = request.ClientToken\n\t}\n\n\tif !dara.IsNil(request.DefaultActions) {\n\t\tquery[\"DefaultActions\"] = request.DefaultActions\n\t}\n\n\tif !dara.IsNil(request.DryRun) {\n\t\tquery[\"DryRun\"] = request.DryRun\n\t}\n\n\tif !dara.IsNil(request.GzipEnabled) {\n\t\tquery[\"GzipEnabled\"] = request.GzipEnabled\n\t}\n\n\tif !dara.IsNil(request.Http2Enabled) {\n\t\tquery[\"Http2Enabled\"] = request.Http2Enabled\n\t}\n\n\tif !dara.IsNil(request.IdleTimeout) {\n\t\tquery[\"IdleTimeout\"] = request.IdleTimeout\n\t}\n\n\tif !dara.IsNil(request.ListenerDescription) {\n\t\tquery[\"ListenerDescription\"] = request.ListenerDescription\n\t}\n\n\tif !dara.IsNil(request.ListenerId) {\n\t\tquery[\"ListenerId\"] = request.ListenerId\n\t}\n\n\tif !dara.IsNil(request.QuicConfig) {\n\t\tquery[\"QuicConfig\"] = request.QuicConfig\n\t}\n\n\tif !dara.IsNil(request.RequestTimeout) {\n\t\tquery[\"RequestTimeout\"] = request.RequestTimeout\n\t}\n\n\tif !dara.IsNil(request.SecurityPolicyId) {\n\t\tquery[\"SecurityPolicyId\"] = request.SecurityPolicyId\n\t}\n\n\tif !dara.IsNil(request.XForwardedForConfig) {\n\t\tquery[\"XForwardedForConfig\"] = request.XForwardedForConfig\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"UpdateListenerAttribute\"),\n\t\tVersion:     dara.String(\"2020-06-16\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alialb.UpdateListenerAttributeResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go",
    "content": "package aliyunapigw\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\taliapig \"github.com/alibabacloud-go/apig-20240327/v6/client\"\n\talicloudapi \"github.com/alibabacloud-go/cloudapi-20160714/v5/client\"\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-apigw/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n\t// 服务类型。\n\tServiceType string `json:\"serviceType\"`\n\t// API 网关 ID。\n\t// 服务类型为 [SERVICE_TYPE_CLOUDNATIVE] 时必填。\n\tGatewayId string `json:\"gatewayId,omitempty\"`\n\t// API 分组 ID。\n\t// 服务类型为 [SERVICE_TYPE_TRADITIONAL] 时必填。\n\tGroupId string `json:\"groupId,omitempty\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 自定义域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClients *wSDKClients\n\tsdkCertmgr certmgr.Provider\n}\n\ntype wSDKClients struct {\n\tCloudNativeAPIGateway *internal.ApigClient\n\tTraditionalAPIGateway *internal.CloudapiClient\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclients, err := createSDKClients(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tResourceGroupId: config.ResourceGroupId,\n\t\tRegion: lo.\n\t\t\tIf(config.Region == \"\" || strings.HasPrefix(config.Region, \"cn-\"), \"cn-hangzhou\").\n\t\t\tElse(\"ap-southeast-1\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClients: clients,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tswitch d.config.ServiceType {\n\tcase SERVICE_TYPE_TRADITIONAL:\n\t\tif err := d.deployToTraditional(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase SERVICE_TYPE_CLOUDNATIVE:\n\t\tif err := d.deployToCloudNative(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported service type '%s'\", string(d.config.ServiceType))\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToTraditional(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.GroupId == \"\" {\n\t\treturn errors.New(\"config `groupId` is required\")\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getTraditionalAllDomainsByGroupId(ctx, d.config.GroupId)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain)\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getTraditionalAllDomainsByGroupId(ctx, d.config.GroupId)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no apigw domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found apigw domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateTraditionalDomainCertificate(ctx, d.config.GroupId, domain, certPEM, privkeyPEM); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToCloudNative(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.GatewayId == \"\" {\n\t\treturn errors.New(\"config `gatewayId` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getCloudNativeAllDomainsByGatewayId(ctx, d.config.GatewayId)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain)\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getCloudNativeAllDomainsByGatewayId(ctx, d.config.GatewayId)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no apigw domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found apigw domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tcertId := upres.ExtendedData[\"CertIdentifier\"].(string)\n\t\t\t\tif err := d.updateCloudNativeDomainCertificate(ctx, d.config.GatewayId, domain, certId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) getTraditionalAllDomainsByGroupId(ctx context.Context, cloudGroupId string) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询 API 分组详情\n\t// REF: https://help.aliyun.com/zh/api-gateway/traditional-api-gateway/developer-reference/api-cloudapi-2016-07-14-describeapigroup\n\tdescribeApiGroupReq := &alicloudapi.DescribeApiGroupRequest{\n\t\tGroupId: tea.String(cloudGroupId),\n\t}\n\tdescribeApiGroupResp, err := d.sdkClients.TraditionalAPIGateway.DescribeApiGroupWithContext(ctx, describeApiGroupReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'apigateway.DescribeApiGroup'\", slog.Any(\"request\", describeApiGroupReq), slog.Any(\"response\", describeApiGroupResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'apigateway.DescribeApiGroup': %w\", err)\n\t}\n\n\tfor _, domainItem := range describeApiGroupResp.Body.CustomDomains.DomainItem {\n\t\tif strings.EqualFold(tea.StringValue(domainItem.DomainBindingStatus), \"BINDING\") {\n\t\t\tdomains = append(domains, tea.StringValue(domainItem.DomainName))\n\t\t}\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) getCloudNativeAllDomainsByGatewayId(ctx context.Context, cloudGatewayId string) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// REF: https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/developer-reference/api-apig-2024-03-27-listdomains\n\tlistDomainsPageNumber := 1\n\tlistDomainsPageSize := 10\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistDomainsReq := &aliapig.ListDomainsRequest{\n\t\t\tResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId),\n\t\t\tGatewayId:       tea.String(cloudGatewayId),\n\t\t\tPageNumber:      tea.Int32(int32(listDomainsPageNumber)),\n\t\t\tPageSize:        tea.Int32(int32(listDomainsPageSize)),\n\t\t}\n\t\tlistDomainsResp, err := d.sdkClients.CloudNativeAPIGateway.ListDomainsWithContext(ctx, listDomainsReq, make(map[string]*string), &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'apig.ListDomains'\", slog.Any(\"request\", listDomainsReq), slog.Any(\"response\", listDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'apig.ListDomains': %w\", err)\n\t\t}\n\n\t\tif listDomainsResp.Body == nil || listDomainsResp.Body.Data == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, domainItem := range listDomainsResp.Body.Data.Items {\n\t\t\tif strings.EqualFold(tea.StringValue(domainItem.Status), \"Published\") {\n\t\t\t\tdomains = append(domains, tea.StringValue(domainItem.Name))\n\t\t\t}\n\t\t}\n\n\t\tif len(listDomainsResp.Body.Data.Items) < listDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tlistDomainsPageNumber++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateTraditionalDomainCertificate(ctx context.Context, cloudGroupId string, domain string, certPEM, privkeyPEM string) error {\n\t// 为自定义域名添加 SSL 证书\n\t// REF: https://help.aliyun.com/zh/api-gateway/traditional-api-gateway/developer-reference/api-cloudapi-2016-07-14-setdomaincertificate\n\tsetDomainCertificateReq := &alicloudapi.SetDomainCertificateRequest{\n\t\tGroupId:               tea.String(cloudGroupId),\n\t\tDomainName:            tea.String(domain),\n\t\tCertificateName:       tea.String(fmt.Sprintf(\"certimate_%d\", time.Now().UnixMilli())),\n\t\tCertificateBody:       tea.String(certPEM),\n\t\tCertificatePrivateKey: tea.String(privkeyPEM),\n\t}\n\tsetDomainCertificateResp, err := d.sdkClients.TraditionalAPIGateway.SetDomainCertificateWithContext(ctx, setDomainCertificateReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'apigateway.SetDomainCertificate'\", slog.Any(\"request\", setDomainCertificateReq), slog.Any(\"response\", setDomainCertificateResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'apigateway.SetDomainCertificate': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateCloudNativeDomainCertificate(ctx context.Context, cloudGatewayId string, domain string, cloudCertId string) error {\n\t// 获取域名 ID\n\tdomainId, err := d.findCloudNativeDomainIdByDomain(ctx, cloudGatewayId, domain)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 查询域名\n\t// REF: https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/developer-reference/api-apig-2024-03-27-getdomain\n\tgetDomainReq := &aliapig.GetDomainRequest{}\n\tgetDomainResp, err := d.sdkClients.CloudNativeAPIGateway.GetDomainWithContext(ctx, tea.String(domainId), getDomainReq, make(map[string]*string), &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'apig.GetDomain'\", slog.String(\"domainId\", domainId), slog.Any(\"request\", getDomainReq), slog.Any(\"response\", getDomainResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'apig.GetDomain': %w\", err)\n\t}\n\n\t// 更新域名\n\t// REF: https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/developer-reference/api-apig-2024-03-27-updatedomain\n\tupdateDomainReq := &aliapig.UpdateDomainRequest{\n\t\tProtocol:              tea.String(\"HTTPS\"),\n\t\tForceHttps:            getDomainResp.Body.Data.ForceHttps,\n\t\tMTLSEnabled:           getDomainResp.Body.Data.MTLSEnabled,\n\t\tHttp2Option:           getDomainResp.Body.Data.Http2Option,\n\t\tTlsMin:                getDomainResp.Body.Data.TlsMin,\n\t\tTlsMax:                getDomainResp.Body.Data.TlsMax,\n\t\tTlsCipherSuitesConfig: getDomainResp.Body.Data.TlsCipherSuitesConfig,\n\t\tCertIdentifier:        tea.String(cloudCertId),\n\t}\n\tupdateDomainResp, err := d.sdkClients.CloudNativeAPIGateway.UpdateDomainWithContext(ctx, tea.String(domainId), updateDomainReq, make(map[string]*string), &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'apig.UpdateDomain'\", slog.String(\"domainId\", domainId), slog.Any(\"request\", updateDomainReq), slog.Any(\"response\", updateDomainResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'apig.UpdateDomain': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) findCloudNativeDomainIdByDomain(ctx context.Context, cloudGatewayId string, domain string) (string, error) {\n\t// 查询域名列表\n\t// REF: https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/developer-reference/api-apig-2024-03-27-listdomains\n\tlistDomainsPageNumber := 1\n\tlistDomainsPageSize := 10\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn \"\", ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistDomainsReq := &aliapig.ListDomainsRequest{\n\t\t\tResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId),\n\t\t\tGatewayId:       tea.String(cloudGatewayId),\n\t\t\tNameLike:        tea.String(domain),\n\t\t\tPageNumber:      tea.Int32(int32(listDomainsPageNumber)),\n\t\t\tPageSize:        tea.Int32(int32(listDomainsPageSize)),\n\t\t}\n\t\tlistDomainsResp, err := d.sdkClients.CloudNativeAPIGateway.ListDomainsWithContext(ctx, listDomainsReq, make(map[string]*string), &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'apig.ListDomains'\", slog.Any(\"request\", listDomainsReq), slog.Any(\"response\", listDomainsResp))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to execute sdk request 'apig.ListDomains': %w\", err)\n\t\t}\n\n\t\tif listDomainsResp.Body == nil || listDomainsResp.Body.Data == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, domainItem := range listDomainsResp.Body.Data.Items {\n\t\t\tif strings.EqualFold(tea.StringValue(domainItem.Name), domain) {\n\t\t\t\treturn tea.StringValue(domainItem.DomainId), nil\n\t\t\t}\n\t\t}\n\n\t\tif len(listDomainsResp.Body.Data.Items) < listDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tlistDomainsPageNumber++\n\t}\n\n\treturn \"\", fmt.Errorf(\"could not find domain '%s'\", domain)\n}\n\nfunc createSDKClients(accessKeyId, accessKeySecret, region string) (*wSDKClients, error) {\n\t// 接入点一览 https://api.aliyun.com/product/APIG\n\tvar cloudNativeAPIGEndpoint string\n\tswitch region {\n\tcase \"\":\n\t\tcloudNativeAPIGEndpoint = \"apig.cn-hangzhou.aliyuncs.com\"\n\tdefault:\n\t\tcloudNativeAPIGEndpoint = fmt.Sprintf(\"apig.%s.aliyuncs.com\", region)\n\t}\n\n\tcloudNativeAPIGConfig := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(cloudNativeAPIGEndpoint),\n\t}\n\tcloudNativeAPIGClient, err := internal.NewApigClient(cloudNativeAPIGConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 接入点一览 https://api.aliyun.com/product/CloudAPI\n\tvar traditionalAPIGEndpoint string\n\tswitch region {\n\tcase \"\":\n\t\ttraditionalAPIGEndpoint = \"apigateway.cn-hangzhou.aliyuncs.com\"\n\tdefault:\n\t\ttraditionalAPIGEndpoint = fmt.Sprintf(\"apigateway.%s.aliyuncs.com\", region)\n\t}\n\n\ttraditionalAPIGConfig := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(traditionalAPIGEndpoint),\n\t}\n\ttraditionalAPIGClient, err := internal.NewCloudapiClient(traditionalAPIGConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &wSDKClients{\n\t\tCloudNativeAPIGateway: cloudNativeAPIGClient,\n\t\tTraditionalAPIGateway: traditionalAPIGClient,\n\t}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw_test.go",
    "content": "package aliyunapigw_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-apigw\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfServiceType     string\n\tfGatewayId       string\n\tfGroupId         string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNAPIGW_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fGatewayId, argsPrefix+\"GATEWARYID\", \"\", \"\")\n\tflag.StringVar(&fGroupId, argsPrefix+\"GROUPID\", \"\", \"\")\n\tflag.StringVar(&fServiceType, argsPrefix+\"SERVICETYPE\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_apigw_test.go -args \\\n\t--ALIYUNAPIGW_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNAPIGW_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNAPIGW_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNAPIGW_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNAPIGW_REGION=\"cn-hangzhou\" \\\n\t--ALIYUNAPIGW_SERVICETYPE=\"cloudnative\" \\\n\t--ALIYUNAPIGW_GATEWAYID=\"your-api-gateway-id\" \\\n\t--ALIYUNAPIGW_GROUPID=\"your-api-group-id\" \\\n\t--ALIYUNAPIGW_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"SERVICETYPE: %v\", fServiceType),\n\t\t\tfmt.Sprintf(\"GATEWAYID: %v\", fGatewayId),\n\t\t\tfmt.Sprintf(\"GROUPID: %v\", fGroupId),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tAccessKeySecret:    fAccessKeySecret,\n\t\t\tRegion:             fRegion,\n\t\t\tServiceType:        fServiceType,\n\t\t\tGatewayId:          fGatewayId,\n\t\t\tGroupId:            fGroupId,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-apigw/consts.go",
    "content": "package aliyunapigw\n\nconst (\n\t// 服务类型：原 API 网关。\n\tSERVICE_TYPE_TRADITIONAL = \"traditional\"\n\t// 服务类型：云原生 API 网关。\n\tSERVICE_TYPE_CLOUDNATIVE = \"cloudnative\"\n)\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-apigw/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\n\taliapig \"github.com/alibabacloud-go/apig-20240327/v6/client\"\n\talicloudapi \"github.com/alibabacloud-go/cloudapi-20160714/v5/client\"\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\topenapiutil \"github.com/alibabacloud-go/darabonba-openapi/v2/utils\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/apig-20240327/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype ApigClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewApigClient(config *openapiutil.Config) (*ApigClient, error) {\n\tclient := new(ApigClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *ApigClient) GetDomainWithContext(ctx context.Context, domainId *string, request *aliapig.GetDomainRequest, headers map[string]*string, runtime *dara.RuntimeOptions) (_result *aliapig.GetDomainResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.WithStatistics) {\n\t\tquery[\"withStatistics\"] = request.WithStatistics\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tHeaders: headers,\n\t\tQuery:   openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"GetDomain\"),\n\t\tVersion:     dara.String(\"2024-03-27\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/v1/domains/\" + dara.PercentEncode(dara.StringValue(domainId))),\n\t\tMethod:      dara.String(\"GET\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"ROA\"),\n\t\tReqBodyType: dara.String(\"json\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &aliapig.GetDomainResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *ApigClient) ListDomainsWithContext(ctx context.Context, request *aliapig.ListDomainsRequest, headers map[string]*string, runtime *dara.RuntimeOptions) (_result *aliapig.ListDomainsResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.GatewayId) {\n\t\tquery[\"gatewayId\"] = request.GatewayId\n\t}\n\n\tif !dara.IsNil(request.GatewayType) {\n\t\tquery[\"gatewayType\"] = request.GatewayType\n\t}\n\n\tif !dara.IsNil(request.NameLike) {\n\t\tquery[\"nameLike\"] = request.NameLike\n\t}\n\n\tif !dara.IsNil(request.PageNumber) {\n\t\tquery[\"pageNumber\"] = request.PageNumber\n\t}\n\n\tif !dara.IsNil(request.PageSize) {\n\t\tquery[\"pageSize\"] = request.PageSize\n\t}\n\n\tif !dara.IsNil(request.ResourceGroupId) {\n\t\tquery[\"resourceGroupId\"] = request.ResourceGroupId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tHeaders: headers,\n\t\tQuery:   openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"ListDomains\"),\n\t\tVersion:     dara.String(\"2024-03-27\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/v1/domains\"),\n\t\tMethod:      dara.String(\"GET\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"ROA\"),\n\t\tReqBodyType: dara.String(\"json\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &aliapig.ListDomainsResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *ApigClient) UpdateDomainWithContext(ctx context.Context, domainId *string, request *aliapig.UpdateDomainRequest, headers map[string]*string, runtime *dara.RuntimeOptions) (_result *aliapig.UpdateDomainResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tbody := map[string]interface{}{}\n\n\tif !dara.IsNil(request.CaCertIdentifier) {\n\t\tbody[\"caCertIdentifier\"] = request.CaCertIdentifier\n\t}\n\n\tif !dara.IsNil(request.CertIdentifier) {\n\t\tbody[\"certIdentifier\"] = request.CertIdentifier\n\t}\n\n\tif !dara.IsNil(request.ClientCACert) {\n\t\tbody[\"clientCACert\"] = request.ClientCACert\n\t}\n\n\tif !dara.IsNil(request.ForceHttps) {\n\t\tbody[\"forceHttps\"] = request.ForceHttps\n\t}\n\n\tif !dara.IsNil(request.Http2Option) {\n\t\tbody[\"http2Option\"] = request.Http2Option\n\t}\n\n\tif !dara.IsNil(request.MTLSEnabled) {\n\t\tbody[\"mTLSEnabled\"] = request.MTLSEnabled\n\t}\n\n\tif !dara.IsNil(request.Protocol) {\n\t\tbody[\"protocol\"] = request.Protocol\n\t}\n\n\tif !dara.IsNil(request.TlsCipherSuitesConfig) {\n\t\tbody[\"tlsCipherSuitesConfig\"] = request.TlsCipherSuitesConfig\n\t}\n\n\tif !dara.IsNil(request.TlsMax) {\n\t\tbody[\"tlsMax\"] = request.TlsMax\n\t}\n\n\tif !dara.IsNil(request.TlsMin) {\n\t\tbody[\"tlsMin\"] = request.TlsMin\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tHeaders: headers,\n\t\tBody:    openapiutil.ParseToMap(body),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"UpdateDomain\"),\n\t\tVersion:     dara.String(\"2024-03-27\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/v1/domains/\" + dara.PercentEncode(dara.StringValue(domainId))),\n\t\tMethod:      dara.String(\"PUT\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"ROA\"),\n\t\tReqBodyType: dara.String(\"json\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &aliapig.UpdateDomainResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\n// This is a partial copy of https://github.com/alibabacloud-go/cloudapi-20160714/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype CloudapiClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewCloudapiClient(config *openapiutil.Config) (*CloudapiClient, error) {\n\tclient := new(CloudapiClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *CloudapiClient) DescribeApiGroupWithContext(ctx context.Context, request *alicloudapi.DescribeApiGroupRequest, runtime *dara.RuntimeOptions) (_result *alicloudapi.DescribeApiGroupResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.GroupId) {\n\t\tquery[\"GroupId\"] = request.GroupId\n\t}\n\n\tif !dara.IsNil(request.SecurityToken) {\n\t\tquery[\"SecurityToken\"] = request.SecurityToken\n\t}\n\n\tif !dara.IsNil(request.Tag) {\n\t\tquery[\"Tag\"] = request.Tag\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DescribeApiGroup\"),\n\t\tVersion:     dara.String(\"2016-07-14\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alicloudapi.DescribeApiGroupResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *CloudapiClient) SetDomainCertificateWithContext(ctx context.Context, request *alicloudapi.SetDomainCertificateRequest, runtime *dara.RuntimeOptions) (_result *alicloudapi.SetDomainCertificateResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.CaCertificateBody) {\n\t\tquery[\"CaCertificateBody\"] = request.CaCertificateBody\n\t}\n\n\tif !dara.IsNil(request.CertificateBody) {\n\t\tquery[\"CertificateBody\"] = request.CertificateBody\n\t}\n\n\tif !dara.IsNil(request.CertificateName) {\n\t\tquery[\"CertificateName\"] = request.CertificateName\n\t}\n\n\tif !dara.IsNil(request.CertificatePrivateKey) {\n\t\tquery[\"CertificatePrivateKey\"] = request.CertificatePrivateKey\n\t}\n\n\tif !dara.IsNil(request.ClientCertSDnPassThrough) {\n\t\tquery[\"ClientCertSDnPassThrough\"] = request.ClientCertSDnPassThrough\n\t}\n\n\tif !dara.IsNil(request.DomainName) {\n\t\tquery[\"DomainName\"] = request.DomainName\n\t}\n\n\tif !dara.IsNil(request.GroupId) {\n\t\tquery[\"GroupId\"] = request.GroupId\n\t}\n\n\tif !dara.IsNil(request.SecurityToken) {\n\t\tquery[\"SecurityToken\"] = request.SecurityToken\n\t}\n\n\tif !dara.IsNil(request.SslOcspCacheEnable) {\n\t\tquery[\"SslOcspCacheEnable\"] = request.SslOcspCacheEnable\n\t}\n\n\tif !dara.IsNil(request.SslOcspEnable) {\n\t\tquery[\"SslOcspEnable\"] = request.SslOcspEnable\n\t}\n\n\tif !dara.IsNil(request.SslVerifyDepth) {\n\t\tquery[\"SslVerifyDepth\"] = request.SslVerifyDepth\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"SetDomainCertificate\"),\n\t\tVersion:     dara.String(\"2016-07-14\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alicloudapi.SetDomainCertificateResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-cas/aliyun_cas.go",
    "content": "package aliyuncas\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tResourceGroupId: config.ResourceGroupId,\n\t\tRegion:          config.Region,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go",
    "content": "package aliyuncasdeploy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\talicas \"github.com/alibabacloud-go/cas-20200407/v4/client\"\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-cas-deploy/internal\"\n\txwait \"github.com/certimate-go/certimate/pkg/utils/wait\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n\t// 云产品资源 ID 数组。\n\tResourceIds []string `json:\"resourceIds\"`\n\t// 云联系人 ID 数组。\n\t// 零值时使用账号下第一个联系人。\n\tContactIds []string `json:\"contactIds\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.CasClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tResourceGroupId: config.ResourceGroupId,\n\t\tRegion:          config.Region,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif len(d.config.ResourceIds) == 0 {\n\t\treturn nil, errors.New(\"config `resourceIds` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\tcontactIds := d.config.ContactIds\n\tif len(contactIds) == 0 {\n\t\t// 获取联系人列表\n\t\t// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-listcontact\n\t\tlistContactReq := &alicas.ListContactRequest{\n\t\t\tShowSize:    tea.Int32(1),\n\t\t\tCurrentPage: tea.Int32(1),\n\t\t}\n\t\tlistContactResp, err := d.sdkClient.ListContactWithContext(ctx, listContactReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'cas.ListContact'\", slog.Any(\"request\", listContactReq), slog.Any(\"response\", listContactResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cas.ListContact': %w\", err)\n\t\t}\n\n\t\tif len(listContactResp.Body.ContactList) > 0 {\n\t\t\tcontactIds = []string{fmt.Sprintf(\"%d\", listContactResp.Body.ContactList[0].ContactId)}\n\t\t}\n\t}\n\n\t// 创建部署任务\n\t// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-createdeploymentjob\n\tcreateDeploymentJobReq := &alicas.CreateDeploymentJobRequest{\n\t\tName:        tea.String(fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())),\n\t\tJobType:     tea.String(\"user\"),\n\t\tCertIds:     tea.String(upres.CertId),\n\t\tResourceIds: tea.String(strings.Join(d.config.ResourceIds, \",\")),\n\t\tContactIds:  tea.String(strings.Join(contactIds, \",\")),\n\t}\n\tcreateDeploymentJobResp, err := d.sdkClient.CreateDeploymentJobWithContext(ctx, createDeploymentJobReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'cas.CreateDeploymentJob'\", slog.Any(\"request\", createDeploymentJobReq), slog.Any(\"response\", createDeploymentJobResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cas.CreateDeploymentJob': %w\", err)\n\t}\n\n\t// 获取部署任务详情，等待任务状态变更\n\t// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-describedeploymentjob\n\tif _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) {\n\t\tdescribeDeploymentJobReq := &alicas.DescribeDeploymentJobRequest{\n\t\t\tJobId: createDeploymentJobResp.Body.JobId,\n\t\t}\n\t\tdescribeDeploymentJobResp, err := d.sdkClient.DescribeDeploymentJobWithContext(ctx, describeDeploymentJobReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'cas.DescribeDeploymentJob'\", slog.Any(\"request\", describeDeploymentJobReq), slog.Any(\"response\", describeDeploymentJobResp))\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to execute sdk request 'cas.DescribeDeploymentJob': %w\", err)\n\t\t}\n\n\t\tswitch tea.StringValue(describeDeploymentJobResp.Body.Status) {\n\t\tcase \"success\", \"error\":\n\t\t\treturn true, nil\n\t\tcase \"\", \"editing\":\n\t\t\treturn false, fmt.Errorf(\"unexpected aliyun deployment job status\")\n\t\t}\n\n\t\td.logger.Info(\"waiting for aliyun deployment job completion ...\")\n\t\treturn false, nil\n\t}, time.Second*5); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.CasClient, error) {\n\t// 接入点一览 https://api.aliyun.com/product/cas\n\tvar endpoint string\n\tswitch region {\n\tcase \"\", \"cn-hangzhou\":\n\t\tendpoint = \"cas.aliyuncs.com\"\n\tdefault:\n\t\tendpoint = fmt.Sprintf(\"cas.%s.aliyuncs.com\", region)\n\t}\n\n\tconfig := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(endpoint),\n\t}\n\n\tclient, err := internal.NewCasClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-cas-deploy/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\n\talicas \"github.com/alibabacloud-go/cas-20200407/v4/client\"\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\topenapiutil \"github.com/alibabacloud-go/darabonba-openapi/v2/utils\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/cas-20200407/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype CasClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewCasClient(config *openapiutil.Config) (*CasClient, error) {\n\tclient := new(CasClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *CasClient) Init(config *openapiutil.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *CasClient) CreateDeploymentJobWithContext(ctx context.Context, request *alicas.CreateDeploymentJobRequest, runtime *dara.RuntimeOptions) (_result *alicas.CreateDeploymentJobResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.CertIds) {\n\t\tquery[\"CertIds\"] = request.CertIds\n\t}\n\n\tif !dara.IsNil(request.ContactIds) {\n\t\tquery[\"ContactIds\"] = request.ContactIds\n\t}\n\n\tif !dara.IsNil(request.JobType) {\n\t\tquery[\"JobType\"] = request.JobType\n\t}\n\n\tif !dara.IsNil(request.Name) {\n\t\tquery[\"Name\"] = request.Name\n\t}\n\n\tif !dara.IsNil(request.ResourceIds) {\n\t\tquery[\"ResourceIds\"] = request.ResourceIds\n\t}\n\n\tif !dara.IsNil(request.ScheduleTime) {\n\t\tquery[\"ScheduleTime\"] = request.ScheduleTime\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"CreateDeploymentJob\"),\n\t\tVersion:     dara.String(\"2020-04-07\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alicas.CreateDeploymentJobResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *CasClient) DescribeDeploymentJobWithContext(ctx context.Context, request *alicas.DescribeDeploymentJobRequest, runtime *dara.RuntimeOptions) (_result *alicas.DescribeDeploymentJobResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.JobId) {\n\t\tquery[\"JobId\"] = request.JobId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DescribeDeploymentJob\"),\n\t\tVersion:     dara.String(\"2020-04-07\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alicas.DescribeDeploymentJobResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *CasClient) ListContactWithContext(ctx context.Context, request *alicas.ListContactRequest, runtime *dara.RuntimeOptions) (_result *alicas.ListContactResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.CurrentPage) {\n\t\tquery[\"CurrentPage\"] = request.CurrentPage\n\t}\n\n\tif !dara.IsNil(request.Keyword) {\n\t\tquery[\"Keyword\"] = request.Keyword\n\t}\n\n\tif !dara.IsNil(request.ShowSize) {\n\t\tquery[\"ShowSize\"] = request.ShowSize\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"ListContact\"),\n\t\tVersion:     dara.String(\"2020-04-07\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alicas.ListContactResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn.go",
    "content": "package aliyuncdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"strings\"\n\n\talicdn \"github.com/alibabacloud-go/cdn-20180510/v9/client\"\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-cdn/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.CdnClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tResourceGroupId: config.ResourceGroupId,\n\t\tRegion: lo.\n\t\t\tIf(config.Region == \"\" || strings.HasPrefix(config.Region, \"cn-\"), \"cn-hangzhou\").\n\t\t\tElse(\"ap-southeast-1\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\t// \"*.example.com\" → \".example.com\"，适配阿里云 CDN 要求的泛域名格式\n\t\t\tdomain := strings.TrimPrefix(d.config.Domain, \"*\")\n\t\t\tdomains = []string{domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain) ||\n\t\t\t\t\t\tstrings.TrimPrefix(d.config.Domain, \"*\") == strings.TrimPrefix(domain, \"*\")\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil ||\n\t\t\t\t\tstrings.TrimPrefix(d.config.Domain, \"*\") == strings.TrimPrefix(domain, \"*\")\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no cdn domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found cdn domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tcertIdentifier := upres.ExtendedData[\"CertIdentifier\"].(string)\n\t\tcertIdentifierSeps := strings.SplitN(certIdentifier, \"-\", 2)\n\t\tif len(certIdentifierSeps) != 2 {\n\t\t\treturn nil, fmt.Errorf(\"received invalid certificate identifier: '%s'\", certIdentifier)\n\t\t}\n\n\t\tcertId, _ := strconv.ParseInt(certIdentifierSeps[0], 10, 64)\n\t\tcertRegion := certIdentifierSeps[1]\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, certId, certRegion); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// REF: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-describeuserdomains\n\tdescribeUserDomainsPageNumber := 1\n\tdescribeUserDomainsPageSize := 500\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeUserDomainsReq := &alicdn.DescribeUserDomainsRequest{\n\t\t\tResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId),\n\t\t\tPageNumber:      tea.Int32(int32(describeUserDomainsPageNumber)),\n\t\t\tPageSize:        tea.Int32(int32(describeUserDomainsPageSize)),\n\t\t}\n\t\tdescribeUserDomainsResp, err := d.sdkClient.DescribeUserDomainsWithContext(ctx, describeUserDomainsReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'cdn.DescribeUserDomains'\", slog.Any(\"request\", describeUserDomainsReq), slog.Any(\"response\", describeUserDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.DescribeUserDomains': %w\", err)\n\t\t}\n\n\t\tif describeUserDomainsResp.Body == nil || describeUserDomainsResp.Body.Domains == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tignoredStatuses := []string{\"offline\", \"checking\", \"check_failed\", \"stopping\", \"deleting\"}\n\t\tfor _, domainItem := range describeUserDomainsResp.Body.Domains.PageData {\n\t\t\tif lo.Contains(ignoredStatuses, tea.StringValue(domainItem.DomainStatus)) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdomains = append(domains, tea.StringValue(domainItem.DomainName))\n\t\t}\n\n\t\tif len(describeUserDomainsResp.Body.Domains.PageData) < describeUserDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeUserDomainsPageNumber++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId int64, certRegion string) error {\n\t// 设置 CDN 域名域名证书\n\t// REF: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-setcdndomainsslcertificate\n\tsetCdnDomainSSLCertificateReq := &alicdn.SetCdnDomainSSLCertificateRequest{\n\t\tDomainName:  tea.String(domain),\n\t\tCertType:    tea.String(\"cas\"),\n\t\tCertId:      tea.Int64(cloudCertId),\n\t\tCertRegion:  tea.String(certRegion),\n\t\tSSLProtocol: tea.String(\"on\"),\n\t}\n\tsetCdnDomainSSLCertificateResp, err := d.sdkClient.SetCdnDomainSSLCertificateWithContext(ctx, setCdnDomainSSLCertificateReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'cdn.SetCdnDomainSSLCertificate'\", slog.Any(\"request\", setCdnDomainSSLCertificateReq), slog.Any(\"response\", setCdnDomainSSLCertificateResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.SetCdnDomainSSLCertificate': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret string) (*internal.CdnClient, error) {\n\tconfig := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(\"cdn.aliyuncs.com\"),\n\t}\n\n\tclient, err := internal.NewCdnClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn_test.go",
    "content": "package aliyuncdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-cdn\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_cdn_test.go -args \\\n\t--ALIYUNCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNCDN_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNCDN_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNCDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tAccessKeySecret:    fAccessKeySecret,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-cdn/consts.go",
    "content": "package aliyuncdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-cdn/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\n\talicdn \"github.com/alibabacloud-go/cdn-20180510/v9/client\"\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\topenapiutil \"github.com/alibabacloud-go/darabonba-openapi/v2/utils\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/cdn-20180510/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype CdnClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewCdnClient(config *openapiutil.Config) (*CdnClient, error) {\n\tclient := new(CdnClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *CdnClient) Init(config *openapiutil.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *CdnClient) DescribeUserDomainsWithContext(ctx context.Context, request *alicdn.DescribeUserDomainsRequest, runtime *dara.RuntimeOptions) (_result *alicdn.DescribeUserDomainsResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.CdnType) {\n\t\tquery[\"CdnType\"] = request.CdnType\n\t}\n\n\tif !dara.IsNil(request.ChangeEndTime) {\n\t\tquery[\"ChangeEndTime\"] = request.ChangeEndTime\n\t}\n\n\tif !dara.IsNil(request.ChangeStartTime) {\n\t\tquery[\"ChangeStartTime\"] = request.ChangeStartTime\n\t}\n\n\tif !dara.IsNil(request.CheckDomainShow) {\n\t\tquery[\"CheckDomainShow\"] = request.CheckDomainShow\n\t}\n\n\tif !dara.IsNil(request.Coverage) {\n\t\tquery[\"Coverage\"] = request.Coverage\n\t}\n\n\tif !dara.IsNil(request.DomainName) {\n\t\tquery[\"DomainName\"] = request.DomainName\n\t}\n\n\tif !dara.IsNil(request.DomainSearchType) {\n\t\tquery[\"DomainSearchType\"] = request.DomainSearchType\n\t}\n\n\tif !dara.IsNil(request.DomainStatus) {\n\t\tquery[\"DomainStatus\"] = request.DomainStatus\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.PageNumber) {\n\t\tquery[\"PageNumber\"] = request.PageNumber\n\t}\n\n\tif !dara.IsNil(request.PageSize) {\n\t\tquery[\"PageSize\"] = request.PageSize\n\t}\n\n\tif !dara.IsNil(request.ResourceGroupId) {\n\t\tquery[\"ResourceGroupId\"] = request.ResourceGroupId\n\t}\n\n\tif !dara.IsNil(request.SecurityToken) {\n\t\tquery[\"SecurityToken\"] = request.SecurityToken\n\t}\n\n\tif !dara.IsNil(request.Source) {\n\t\tquery[\"Source\"] = request.Source\n\t}\n\n\tif !dara.IsNil(request.Tag) {\n\t\tquery[\"Tag\"] = request.Tag\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DescribeUserDomains\"),\n\t\tVersion:     dara.String(\"2018-05-10\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alicdn.DescribeUserDomainsResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *CdnClient) SetCdnDomainSSLCertificateWithContext(ctx context.Context, request *alicdn.SetCdnDomainSSLCertificateRequest, runtime *dara.RuntimeOptions) (_result *alicdn.SetCdnDomainSSLCertificateResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.CertId) {\n\t\tquery[\"CertId\"] = request.CertId\n\t}\n\n\tif !dara.IsNil(request.CertName) {\n\t\tquery[\"CertName\"] = request.CertName\n\t}\n\n\tif !dara.IsNil(request.CertRegion) {\n\t\tquery[\"CertRegion\"] = request.CertRegion\n\t}\n\n\tif !dara.IsNil(request.CertType) {\n\t\tquery[\"CertType\"] = request.CertType\n\t}\n\n\tif !dara.IsNil(request.DomainName) {\n\t\tquery[\"DomainName\"] = request.DomainName\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.SSLPri) {\n\t\tquery[\"SSLPri\"] = request.SSLPri\n\t}\n\n\tif !dara.IsNil(request.SSLProtocol) {\n\t\tquery[\"SSLProtocol\"] = request.SSLProtocol\n\t}\n\n\tif !dara.IsNil(request.SSLPub) {\n\t\tquery[\"SSLPub\"] = request.SSLPub\n\t}\n\n\tif !dara.IsNil(request.SecurityToken) {\n\t\tquery[\"SecurityToken\"] = request.SecurityToken\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"SetCdnDomainSSLCertificate\"),\n\t\tVersion:     dara.String(\"2018-05-10\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alicdn.SetCdnDomainSSLCertificateResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go",
    "content": "package aliyunclb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\talislb \"github.com/alibabacloud-go/slb-20140515/v4/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-slb\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-clb/internal\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 负载均衡实例 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时必填。\n\tLoadbalancerId string `json:\"loadbalancerId,omitempty\"`\n\t// 负载均衡监听端口。\n\t// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。\n\tListenerPort int32 `json:\"listenerPort,omitempty\"`\n\t// SNI 域名（支持泛域名）。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。\n\tDomain string `json:\"domain,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.SlbClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tResourceGroupId: config.ResourceGroupId,\n\t\tRegion:          config.Region,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_LOADBALANCER:\n\t\tif err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_LISTENER:\n\t\tif err := d.deployToListener(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\n\t// 查询负载均衡实例的详细信息\n\t// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerattribute\n\tdescribeLoadBalancerAttributeReq := &alislb.DescribeLoadBalancerAttributeRequest{\n\t\tRegionId:       tea.String(d.config.Region),\n\t\tLoadBalancerId: tea.String(d.config.LoadbalancerId),\n\t}\n\tdescribeLoadBalancerAttributeResp, err := d.sdkClient.DescribeLoadBalancerAttributeWithContext(ctx, describeLoadBalancerAttributeReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'slb.DescribeLoadBalancerAttribute'\", slog.Any(\"request\", describeLoadBalancerAttributeReq), slog.Any(\"response\", describeLoadBalancerAttributeResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'slb.DescribeLoadBalancerAttribute': %w\", err)\n\t}\n\n\t// 查询 HTTPS 监听列表\n\t// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerlisteners\n\tlistenerPorts := make([]int32, 0)\n\tdescribeLoadBalancerListenersToken := (*string)(nil)\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeLoadBalancerListenersReq := &alislb.DescribeLoadBalancerListenersRequest{\n\t\t\tRegionId:         tea.String(d.config.Region),\n\t\t\tNextToken:        describeLoadBalancerListenersToken,\n\t\t\tMaxResults:       tea.Int32(100),\n\t\t\tLoadBalancerId:   tea.StringSlice([]string{d.config.LoadbalancerId}),\n\t\t\tListenerProtocol: tea.String(\"https\"),\n\t\t}\n\t\tdescribeLoadBalancerListenersResp, err := d.sdkClient.DescribeLoadBalancerListenersWithContext(ctx, describeLoadBalancerListenersReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'slb.DescribeLoadBalancerListeners'\", slog.Any(\"request\", describeLoadBalancerListenersReq), slog.Any(\"response\", describeLoadBalancerListenersResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'slb.DescribeLoadBalancerListeners': %w\", err)\n\t\t}\n\n\t\tif describeLoadBalancerListenersResp.Body == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, listener := range describeLoadBalancerListenersResp.Body.Listeners {\n\t\t\tlistenerPorts = append(listenerPorts, *listener.ListenerPort)\n\t\t}\n\n\t\tif len(describeLoadBalancerListenersResp.Body.Listeners) == 0 || describeLoadBalancerListenersResp.Body.NextToken == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeLoadBalancerListenersToken = describeLoadBalancerListenersResp.Body.NextToken\n\t}\n\n\t// 遍历更新监听证书\n\tif len(listenerPorts) == 0 {\n\t\td.logger.Info(\"no clb listeners to deploy\")\n\t} else {\n\t\td.logger.Info(\"found https listeners to deploy\", slog.Any(\"listenerPorts\", listenerPorts))\n\t\tvar errs []error\n\n\t\tfor _, listenerPort := range listenerPorts {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listenerPort, cloudCertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\tif d.config.ListenerPort == 0 {\n\t\treturn errors.New(\"config `listenerPort` is required\")\n\t}\n\n\t// 更新监听\n\tif err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, d.config.ListenerPort, cloudCertId); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudListenerPort int32, cloudCertId string) error {\n\t// 查询监听配置\n\t// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerhttpslistenerattribute\n\tdescribeLoadBalancerHTTPSListenerAttributeReq := &alislb.DescribeLoadBalancerHTTPSListenerAttributeRequest{\n\t\tLoadBalancerId: tea.String(cloudLoadbalancerId),\n\t\tListenerPort:   tea.Int32(cloudListenerPort),\n\t}\n\tdescribeLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.DescribeLoadBalancerHTTPSListenerAttributeWithContext(ctx, describeLoadBalancerHTTPSListenerAttributeReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute'\", slog.Any(\"request\", describeLoadBalancerHTTPSListenerAttributeReq), slog.Any(\"response\", describeLoadBalancerHTTPSListenerAttributeResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute': %w\", err)\n\t}\n\n\tif d.config.Domain == \"\" {\n\t\t// 未指定 SNI，只需部署到监听器\n\n\t\t// 修改监听配置\n\t\t// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setloadbalancerhttpslistenerattribute\n\t\tsetLoadBalancerHTTPSListenerAttributeReq := &alislb.SetLoadBalancerHTTPSListenerAttributeRequest{\n\t\t\tRegionId:            tea.String(d.config.Region),\n\t\t\tLoadBalancerId:      tea.String(cloudLoadbalancerId),\n\t\t\tListenerPort:        tea.Int32(cloudListenerPort),\n\t\t\tServerCertificateId: tea.String(cloudCertId),\n\t\t}\n\t\tsetLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.SetLoadBalancerHTTPSListenerAttributeWithContext(ctx, setLoadBalancerHTTPSListenerAttributeReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute'\", slog.Any(\"request\", setLoadBalancerHTTPSListenerAttributeReq), slog.Any(\"response\", setLoadBalancerHTTPSListenerAttributeResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute': %w\", err)\n\t\t}\n\t} else {\n\t\t// 指定 SNI，需部署到扩展域名\n\n\t\t// 查询扩展域名\n\t\t// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describedomainextensions\n\t\tdescribeDomainExtensionsReq := &alislb.DescribeDomainExtensionsRequest{\n\t\t\tRegionId:       tea.String(d.config.Region),\n\t\t\tLoadBalancerId: tea.String(cloudLoadbalancerId),\n\t\t\tListenerPort:   tea.Int32(cloudListenerPort),\n\t\t}\n\t\tdescribeDomainExtensionsResp, err := d.sdkClient.DescribeDomainExtensionsWithContext(ctx, describeDomainExtensionsReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'slb.DescribeDomainExtensions'\", slog.Any(\"request\", describeDomainExtensionsReq), slog.Any(\"response\", describeDomainExtensionsResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'slb.DescribeDomainExtensions': %w\", err)\n\t\t}\n\n\t\t// 遍历修改扩展域名证书\n\t\t// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setdomainextensionattribute\n\t\tif describeDomainExtensionsResp.Body.DomainExtensions != nil && describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension != nil {\n\t\t\tvar errs []error\n\n\t\t\tfor _, domainExtension := range describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension {\n\t\t\t\tif *domainExtension.Domain != d.config.Domain {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tsetDomainExtensionAttributeReq := &alislb.SetDomainExtensionAttributeRequest{\n\t\t\t\t\tRegionId:            tea.String(d.config.Region),\n\t\t\t\t\tDomainExtensionId:   tea.String(*domainExtension.DomainExtensionId),\n\t\t\t\t\tServerCertificateId: tea.String(cloudCertId),\n\t\t\t\t}\n\t\t\t\tsetDomainExtensionAttributeResp, err := d.sdkClient.SetDomainExtensionAttributeWithContext(ctx, setDomainExtensionAttributeReq, &dara.RuntimeOptions{})\n\t\t\t\td.logger.Debug(\"sdk request 'slb.SetDomainExtensionAttribute'\", slog.Any(\"request\", setDomainExtensionAttributeReq), slog.Any(\"response\", setDomainExtensionAttributeResp))\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs = append(errs, fmt.Errorf(\"failed to execute sdk request 'slb.SetDomainExtensionAttribute': %w\", err))\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(errs) > 0 {\n\t\t\t\treturn errors.Join(errs...)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.SlbClient, error) {\n\t// 接入点一览 https://api.aliyun.com/product/Slb\n\tvar endpoint string\n\tswitch region {\n\tcase \"\",\n\t\t\"cn-hangzhou\",\n\t\t\"cn-hangzhou-finance\",\n\t\t\"cn-shanghai-finance-1\",\n\t\t\"cn-shenzhen-finance-1\":\n\t\tendpoint = \"slb.aliyuncs.com\"\n\tdefault:\n\t\tendpoint = fmt.Sprintf(\"slb.%s.aliyuncs.com\", region)\n\t}\n\n\tconfig := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(endpoint),\n\t}\n\n\tclient, err := internal.NewSlbClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-clb/aliyun_clb_test.go",
    "content": "package aliyunclb_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-clb\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfLoadbalancerId  string\n\tfListenerPort    int64\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNCLB_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fLoadbalancerId, argsPrefix+\"LOADBALANCERID\", \"\", \"\")\n\tflag.Int64Var(&fListenerPort, argsPrefix+\"LISTENERPORT\", 443, \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_clb_test.go -args \\\n\t--ALIYUNCLB_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNCLB_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNCLB_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNCLB_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNCLB_REGION=\"cn-hangzhou\" \\\n\t--ALIYUNCLB_LOADBALANCERID=\"your-clb-instance-id\" \\\n\t--ALIYUNCLB_LISTENERPORT=443 \\\n\t--ALIYUNCLB_DOMAIN=\"your-clb-sni-domain\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy_ToLoadbalancer\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LOADBALANCERID: %v\", fLoadbalancerId),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegion:          fRegion,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LOADBALANCER,\n\t\t\tLoadbalancerId:  fLoadbalancerId,\n\t\t\tDomain:          fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n\n\tt.Run(\"Deploy_ToListener\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LOADBALANCERID: %v\", fLoadbalancerId),\n\t\t\tfmt.Sprintf(\"LISTENERPORT: %v\", fListenerPort),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegion:          fRegion,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LISTENER,\n\t\t\tLoadbalancerId:  fLoadbalancerId,\n\t\t\tListenerPort:    int32(fListenerPort),\n\t\t\tDomain:          fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-clb/consts.go",
    "content": "package aliyunclb\n\nconst (\n\t// 资源类型：部署到指定负载均衡器。\n\tRESOURCE_TYPE_LOADBALANCER = \"loadbalancer\"\n\t// 资源类型：部署到指定监听器。\n\tRESOURCE_TYPE_LISTENER = \"listener\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-clb/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\topenapiutil \"github.com/alibabacloud-go/darabonba-openapi/v2/utils\"\n\talislb \"github.com/alibabacloud-go/slb-20140515/v4/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/slb-20140515/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype SlbClient struct {\n\topenapi.Client\n}\n\nfunc NewSlbClient(config *openapi.Config) (*SlbClient, error) {\n\tclient := new(SlbClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *SlbClient) Init(config *openapi.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *SlbClient) DescribeDomainExtensionsWithContext(ctx context.Context, request *alislb.DescribeDomainExtensionsRequest, runtime *dara.RuntimeOptions) (_result *alislb.DescribeDomainExtensionsResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\n\tquery := map[string]interface{}{}\n\tif !dara.IsNil(request.DomainExtensionId) {\n\t\tquery[\"DomainExtensionId\"] = request.DomainExtensionId\n\t}\n\n\tif !dara.IsNil(request.ListenerPort) {\n\t\tquery[\"ListenerPort\"] = request.ListenerPort\n\t}\n\n\tif !dara.IsNil(request.LoadBalancerId) {\n\t\tquery[\"LoadBalancerId\"] = request.LoadBalancerId\n\t}\n\n\tif !dara.IsNil(request.OwnerAccount) {\n\t\tquery[\"OwnerAccount\"] = request.OwnerAccount\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !dara.IsNil(request.ResourceOwnerAccount) {\n\t\tquery[\"ResourceOwnerAccount\"] = request.ResourceOwnerAccount\n\t}\n\n\tif !dara.IsNil(request.ResourceOwnerId) {\n\t\tquery[\"ResourceOwnerId\"] = request.ResourceOwnerId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DescribeDomainExtensions\"),\n\t\tVersion:     dara.String(\"2014-05-15\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alislb.DescribeDomainExtensionsResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *SlbClient) DescribeLoadBalancerListenersWithContext(ctx context.Context, request *alislb.DescribeLoadBalancerListenersRequest, runtime *dara.RuntimeOptions) (_result *alislb.DescribeLoadBalancerListenersResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\n\tquery := map[string]interface{}{}\n\tif !dara.IsNil(request.Description) {\n\t\tquery[\"Description\"] = request.Description\n\t}\n\n\tif !dara.IsNil(request.ListenerPort) {\n\t\tquery[\"ListenerPort\"] = request.ListenerPort\n\t}\n\n\tif !dara.IsNil(request.ListenerProtocol) {\n\t\tquery[\"ListenerProtocol\"] = request.ListenerProtocol\n\t}\n\n\tif !dara.IsNil(request.LoadBalancerId) {\n\t\tquery[\"LoadBalancerId\"] = request.LoadBalancerId\n\t}\n\n\tif !dara.IsNil(request.MaxResults) {\n\t\tquery[\"MaxResults\"] = request.MaxResults\n\t}\n\n\tif !dara.IsNil(request.NextToken) {\n\t\tquery[\"NextToken\"] = request.NextToken\n\t}\n\n\tif !dara.IsNil(request.OwnerAccount) {\n\t\tquery[\"OwnerAccount\"] = request.OwnerAccount\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !dara.IsNil(request.ResourceOwnerAccount) {\n\t\tquery[\"ResourceOwnerAccount\"] = request.ResourceOwnerAccount\n\t}\n\n\tif !dara.IsNil(request.ResourceOwnerId) {\n\t\tquery[\"ResourceOwnerId\"] = request.ResourceOwnerId\n\t}\n\n\tif !dara.IsNil(request.Tag) {\n\t\tquery[\"Tag\"] = request.Tag\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DescribeLoadBalancerListeners\"),\n\t\tVersion:     dara.String(\"2014-05-15\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alislb.DescribeLoadBalancerListenersResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *SlbClient) DescribeLoadBalancerAttributeWithContext(ctx context.Context, request *alislb.DescribeLoadBalancerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alislb.DescribeLoadBalancerAttributeResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\n\tquery := map[string]interface{}{}\n\tif !dara.IsNil(request.LoadBalancerId) {\n\t\tquery[\"LoadBalancerId\"] = request.LoadBalancerId\n\t}\n\n\tif !dara.IsNil(request.OwnerAccount) {\n\t\tquery[\"OwnerAccount\"] = request.OwnerAccount\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !dara.IsNil(request.ResourceOwnerAccount) {\n\t\tquery[\"ResourceOwnerAccount\"] = request.ResourceOwnerAccount\n\t}\n\n\tif !dara.IsNil(request.ResourceOwnerId) {\n\t\tquery[\"ResourceOwnerId\"] = request.ResourceOwnerId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DescribeLoadBalancerAttribute\"),\n\t\tVersion:     dara.String(\"2014-05-15\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alislb.DescribeLoadBalancerAttributeResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *SlbClient) DescribeLoadBalancerHTTPSListenerAttributeWithContext(ctx context.Context, request *alislb.DescribeLoadBalancerHTTPSListenerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alislb.DescribeLoadBalancerHTTPSListenerAttributeResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\n\tquery := map[string]interface{}{}\n\tif !dara.IsNil(request.ListenerPort) {\n\t\tquery[\"ListenerPort\"] = request.ListenerPort\n\t}\n\n\tif !dara.IsNil(request.LoadBalancerId) {\n\t\tquery[\"LoadBalancerId\"] = request.LoadBalancerId\n\t}\n\n\tif !dara.IsNil(request.OwnerAccount) {\n\t\tquery[\"OwnerAccount\"] = request.OwnerAccount\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !dara.IsNil(request.ResourceOwnerAccount) {\n\t\tquery[\"ResourceOwnerAccount\"] = request.ResourceOwnerAccount\n\t}\n\n\tif !dara.IsNil(request.ResourceOwnerId) {\n\t\tquery[\"ResourceOwnerId\"] = request.ResourceOwnerId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DescribeLoadBalancerHTTPSListenerAttribute\"),\n\t\tVersion:     dara.String(\"2014-05-15\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alislb.DescribeLoadBalancerHTTPSListenerAttributeResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *SlbClient) SetDomainExtensionAttributeWithContext(ctx context.Context, request *alislb.SetDomainExtensionAttributeRequest, runtime *dara.RuntimeOptions) (_result *alislb.SetDomainExtensionAttributeResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\n\tquery := map[string]interface{}{}\n\tif !dara.IsNil(request.DomainExtensionId) {\n\t\tquery[\"DomainExtensionId\"] = request.DomainExtensionId\n\t}\n\n\tif !dara.IsNil(request.OwnerAccount) {\n\t\tquery[\"OwnerAccount\"] = request.OwnerAccount\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !dara.IsNil(request.ResourceOwnerAccount) {\n\t\tquery[\"ResourceOwnerAccount\"] = request.ResourceOwnerAccount\n\t}\n\n\tif !dara.IsNil(request.ResourceOwnerId) {\n\t\tquery[\"ResourceOwnerId\"] = request.ResourceOwnerId\n\t}\n\n\tif !dara.IsNil(request.ServerCertificateId) {\n\t\tquery[\"ServerCertificateId\"] = request.ServerCertificateId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"SetDomainExtensionAttribute\"),\n\t\tVersion:     dara.String(\"2014-05-15\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alislb.SetDomainExtensionAttributeResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *SlbClient) SetLoadBalancerHTTPSListenerAttributeWithContext(ctx context.Context, request *alislb.SetLoadBalancerHTTPSListenerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alislb.SetLoadBalancerHTTPSListenerAttributeResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\n\tquery := map[string]interface{}{}\n\tif !dara.IsNil(request.AclId) {\n\t\tquery[\"AclId\"] = request.AclId\n\t}\n\n\tif !dara.IsNil(request.AclStatus) {\n\t\tquery[\"AclStatus\"] = request.AclStatus\n\t}\n\n\tif !dara.IsNil(request.AclType) {\n\t\tquery[\"AclType\"] = request.AclType\n\t}\n\n\tif !dara.IsNil(request.Bandwidth) {\n\t\tquery[\"Bandwidth\"] = request.Bandwidth\n\t}\n\n\tif !dara.IsNil(request.CACertificateId) {\n\t\tquery[\"CACertificateId\"] = request.CACertificateId\n\t}\n\n\tif !dara.IsNil(request.Cookie) {\n\t\tquery[\"Cookie\"] = request.Cookie\n\t}\n\n\tif !dara.IsNil(request.CookieTimeout) {\n\t\tquery[\"CookieTimeout\"] = request.CookieTimeout\n\t}\n\n\tif !dara.IsNil(request.Description) {\n\t\tquery[\"Description\"] = request.Description\n\t}\n\n\tif !dara.IsNil(request.EnableHttp2) {\n\t\tquery[\"EnableHttp2\"] = request.EnableHttp2\n\t}\n\n\tif !dara.IsNil(request.Gzip) {\n\t\tquery[\"Gzip\"] = request.Gzip\n\t}\n\n\tif !dara.IsNil(request.HealthCheck) {\n\t\tquery[\"HealthCheck\"] = request.HealthCheck\n\t}\n\n\tif !dara.IsNil(request.HealthCheckConnectPort) {\n\t\tquery[\"HealthCheckConnectPort\"] = request.HealthCheckConnectPort\n\t}\n\n\tif !dara.IsNil(request.HealthCheckDomain) {\n\t\tquery[\"HealthCheckDomain\"] = request.HealthCheckDomain\n\t}\n\n\tif !dara.IsNil(request.HealthCheckHttpCode) {\n\t\tquery[\"HealthCheckHttpCode\"] = request.HealthCheckHttpCode\n\t}\n\n\tif !dara.IsNil(request.HealthCheckInterval) {\n\t\tquery[\"HealthCheckInterval\"] = request.HealthCheckInterval\n\t}\n\n\tif !dara.IsNil(request.HealthCheckMethod) {\n\t\tquery[\"HealthCheckMethod\"] = request.HealthCheckMethod\n\t}\n\n\tif !dara.IsNil(request.HealthCheckTimeout) {\n\t\tquery[\"HealthCheckTimeout\"] = request.HealthCheckTimeout\n\t}\n\n\tif !dara.IsNil(request.HealthCheckURI) {\n\t\tquery[\"HealthCheckURI\"] = request.HealthCheckURI\n\t}\n\n\tif !dara.IsNil(request.HealthyThreshold) {\n\t\tquery[\"HealthyThreshold\"] = request.HealthyThreshold\n\t}\n\n\tif !dara.IsNil(request.IdleTimeout) {\n\t\tquery[\"IdleTimeout\"] = request.IdleTimeout\n\t}\n\n\tif !dara.IsNil(request.ListenerPort) {\n\t\tquery[\"ListenerPort\"] = request.ListenerPort\n\t}\n\n\tif !dara.IsNil(request.LoadBalancerId) {\n\t\tquery[\"LoadBalancerId\"] = request.LoadBalancerId\n\t}\n\n\tif !dara.IsNil(request.OwnerAccount) {\n\t\tquery[\"OwnerAccount\"] = request.OwnerAccount\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !dara.IsNil(request.RequestTimeout) {\n\t\tquery[\"RequestTimeout\"] = request.RequestTimeout\n\t}\n\n\tif !dara.IsNil(request.ResourceOwnerAccount) {\n\t\tquery[\"ResourceOwnerAccount\"] = request.ResourceOwnerAccount\n\t}\n\n\tif !dara.IsNil(request.ResourceOwnerId) {\n\t\tquery[\"ResourceOwnerId\"] = request.ResourceOwnerId\n\t}\n\n\tif !dara.IsNil(request.Scheduler) {\n\t\tquery[\"Scheduler\"] = request.Scheduler\n\t}\n\n\tif !dara.IsNil(request.ServerCertificateId) {\n\t\tquery[\"ServerCertificateId\"] = request.ServerCertificateId\n\t}\n\n\tif !dara.IsNil(request.StickySession) {\n\t\tquery[\"StickySession\"] = request.StickySession\n\t}\n\n\tif !dara.IsNil(request.StickySessionType) {\n\t\tquery[\"StickySessionType\"] = request.StickySessionType\n\t}\n\n\tif !dara.IsNil(request.TLSCipherPolicy) {\n\t\tquery[\"TLSCipherPolicy\"] = request.TLSCipherPolicy\n\t}\n\n\tif !dara.IsNil(request.UnhealthyThreshold) {\n\t\tquery[\"UnhealthyThreshold\"] = request.UnhealthyThreshold\n\t}\n\n\tif !dara.IsNil(request.VServerGroup) {\n\t\tquery[\"VServerGroup\"] = request.VServerGroup\n\t}\n\n\tif !dara.IsNil(request.VServerGroupId) {\n\t\tquery[\"VServerGroupId\"] = request.VServerGroupId\n\t}\n\n\tif !dara.IsNil(request.XForwardedFor) {\n\t\tquery[\"XForwardedFor\"] = request.XForwardedFor\n\t}\n\n\tif !dara.IsNil(request.XForwardedFor_ClientSrcPort) {\n\t\tquery[\"XForwardedFor_ClientSrcPort\"] = request.XForwardedFor_ClientSrcPort\n\t}\n\n\tif !dara.IsNil(request.XForwardedFor_SLBID) {\n\t\tquery[\"XForwardedFor_SLBID\"] = request.XForwardedFor_SLBID\n\t}\n\n\tif !dara.IsNil(request.XForwardedFor_SLBIP) {\n\t\tquery[\"XForwardedFor_SLBIP\"] = request.XForwardedFor_SLBIP\n\t}\n\n\tif !dara.IsNil(request.XForwardedFor_SLBPORT) {\n\t\tquery[\"XForwardedFor_SLBPORT\"] = request.XForwardedFor_SLBPORT\n\t}\n\n\tif !dara.IsNil(request.XForwardedFor_proto) {\n\t\tquery[\"XForwardedFor_proto\"] = request.XForwardedFor_proto\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"SetLoadBalancerHTTPSListenerAttribute\"),\n\t\tVersion:     dara.String(\"2014-05-15\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alislb.SetLoadBalancerHTTPSListenerAttributeResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn.go",
    "content": "package aliyundcdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"strings\"\n\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\talidcdn \"github.com/alibabacloud-go/dcdn-20180115/v4/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-dcdn/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.DcdnClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tResourceGroupId: config.ResourceGroupId,\n\t\tRegion: lo.\n\t\t\tIf(config.Region == \"\" || strings.HasPrefix(config.Region, \"cn-\"), \"cn-hangzhou\").\n\t\t\tElse(\"ap-southeast-1\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\t// \"*.example.com\" → \".example.com\"，适配阿里云 DCDN 要求的泛域名格式\n\t\t\tdomain := strings.TrimPrefix(d.config.Domain, \"*\")\n\t\t\tdomains = []string{domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain) ||\n\t\t\t\t\t\tstrings.TrimPrefix(d.config.Domain, \"*\") == strings.TrimPrefix(domain, \"*\")\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil ||\n\t\t\t\t\tstrings.TrimPrefix(d.config.Domain, \"*\") == strings.TrimPrefix(domain, \"*\")\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no dcdn domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found dcdn domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tcertIdentifier := upres.ExtendedData[\"CertIdentifier\"].(string)\n\t\tcertIdentifierSeps := strings.SplitN(certIdentifier, \"-\", 2)\n\t\tif len(certIdentifierSeps) != 2 {\n\t\t\treturn nil, fmt.Errorf(\"received invalid certificate identifier: '%s'\", certIdentifier)\n\t\t}\n\n\t\tcertId, _ := strconv.ParseInt(certIdentifierSeps[0], 10, 64)\n\t\tcertRegion := certIdentifierSeps[1]\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, certId, certRegion); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// REF: https://help.aliyun.com/zh/edge-security-acceleration/dcdn/developer-reference/api-dcdn-2018-01-15-describedcdnuserdomains\n\tdescribeUserDomainsPageNumber := 1\n\tdescribeUserDomainsPageSize := 500\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeDcdnUserDomainsReq := &alidcdn.DescribeDcdnUserDomainsRequest{\n\t\t\tResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId),\n\t\t\tCheckDomainShow: tea.Bool(true),\n\t\t\tPageNumber:      tea.Int32(int32(describeUserDomainsPageNumber)),\n\t\t\tPageSize:        tea.Int32(int32(describeUserDomainsPageSize)),\n\t\t}\n\t\tdescribeDcdnUserDomainsResp, err := d.sdkClient.DescribeDcdnUserDomainsWithContext(ctx, describeDcdnUserDomainsReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'dcdn.DescribeDcdnUserDomains'\", slog.Any(\"request\", describeDcdnUserDomainsReq), slog.Any(\"response\", describeDcdnUserDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'dcdn.DescribeDcdnUserDomains': %w\", err)\n\t\t}\n\n\t\tif describeDcdnUserDomainsResp.Body == nil || describeDcdnUserDomainsResp.Body.Domains == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tignoredStatuses := []string{\"offline\", \"checking\", \"check_failed\", \"stopping\", \"deleting\"}\n\t\tfor _, domainItem := range describeDcdnUserDomainsResp.Body.Domains.PageData {\n\t\t\tif lo.Contains(ignoredStatuses, tea.StringValue(domainItem.DomainStatus)) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdomains = append(domains, tea.StringValue(domainItem.DomainName))\n\t\t}\n\n\t\tif len(describeDcdnUserDomainsResp.Body.Domains.PageData) < describeUserDomainsPageNumber {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeUserDomainsPageNumber++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId int64, certRegion string) error {\n\t// 配置域名证书\n\t// REF: https://help.aliyun.com/zh/edge-security-acceleration/dcdn/developer-reference/api-dcdn-2018-01-15-setdcdndomainsslcertificate\n\tsetDcdnDomainSSLCertificateReq := &alidcdn.SetDcdnDomainSSLCertificateRequest{\n\t\tDomainName:  tea.String(domain),\n\t\tCertType:    tea.String(\"cas\"),\n\t\tCertId:      tea.Int64(cloudCertId),\n\t\tCertRegion:  tea.String(certRegion),\n\t\tSSLProtocol: tea.String(\"on\"),\n\t}\n\tsetDcdnDomainSSLCertificateResp, err := d.sdkClient.SetDcdnDomainSSLCertificateWithContext(ctx, setDcdnDomainSSLCertificateReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'dcdn.SetDcdnDomainSSLCertificate'\", slog.Any(\"request\", setDcdnDomainSSLCertificateReq), slog.Any(\"response\", setDcdnDomainSSLCertificateResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'dcdn.SetDcdnDomainSSLCertificate': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret string) (*internal.DcdnClient, error) {\n\tconfig := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(\"dcdn.aliyuncs.com\"),\n\t}\n\n\tclient, err := internal.NewDcdnClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn_test.go",
    "content": "package aliyundcdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-dcdn\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNDCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_dcdn_test.go -args \\\n\t--ALIYUNDCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNDCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNDCDN_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNDCDN_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNDCDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tAccessKeySecret:    fAccessKeySecret,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-dcdn/consts.go",
    "content": "package aliyundcdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-dcdn/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\topenapiutil \"github.com/alibabacloud-go/darabonba-openapi/v2/utils\"\n\talidcdn \"github.com/alibabacloud-go/dcdn-20180115/v4/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/dcdn-20180115/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype DcdnClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewDcdnClient(config *openapiutil.Config) (*DcdnClient, error) {\n\tclient := new(DcdnClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *DcdnClient) Init(config *openapiutil.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *DcdnClient) DescribeDcdnUserDomainsWithContext(ctx context.Context, request *alidcdn.DescribeDcdnUserDomainsRequest, runtime *dara.RuntimeOptions) (_result *alidcdn.DescribeDcdnUserDomainsResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.ChangeEndTime) {\n\t\tquery[\"ChangeEndTime\"] = request.ChangeEndTime\n\t}\n\n\tif !dara.IsNil(request.ChangeStartTime) {\n\t\tquery[\"ChangeStartTime\"] = request.ChangeStartTime\n\t}\n\n\tif !dara.IsNil(request.CheckDomainShow) {\n\t\tquery[\"CheckDomainShow\"] = request.CheckDomainShow\n\t}\n\n\tif !dara.IsNil(request.Coverage) {\n\t\tquery[\"Coverage\"] = request.Coverage\n\t}\n\n\tif !dara.IsNil(request.DomainName) {\n\t\tquery[\"DomainName\"] = request.DomainName\n\t}\n\n\tif !dara.IsNil(request.DomainSearchType) {\n\t\tquery[\"DomainSearchType\"] = request.DomainSearchType\n\t}\n\n\tif !dara.IsNil(request.DomainStatus) {\n\t\tquery[\"DomainStatus\"] = request.DomainStatus\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.PageNumber) {\n\t\tquery[\"PageNumber\"] = request.PageNumber\n\t}\n\n\tif !dara.IsNil(request.PageSize) {\n\t\tquery[\"PageSize\"] = request.PageSize\n\t}\n\n\tif !dara.IsNil(request.ResourceGroupId) {\n\t\tquery[\"ResourceGroupId\"] = request.ResourceGroupId\n\t}\n\n\tif !dara.IsNil(request.SecurityToken) {\n\t\tquery[\"SecurityToken\"] = request.SecurityToken\n\t}\n\n\tif !dara.IsNil(request.Tag) {\n\t\tquery[\"Tag\"] = request.Tag\n\t}\n\n\tif !dara.IsNil(request.WebSiteType) {\n\t\tquery[\"WebSiteType\"] = request.WebSiteType\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DescribeDcdnUserDomains\"),\n\t\tVersion:     dara.String(\"2018-01-15\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alidcdn.DescribeDcdnUserDomainsResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *DcdnClient) SetDcdnDomainSSLCertificateWithContext(ctx context.Context, request *alidcdn.SetDcdnDomainSSLCertificateRequest, runtime *dara.RuntimeOptions) (_result *alidcdn.SetDcdnDomainSSLCertificateResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.CertId) {\n\t\tquery[\"CertId\"] = request.CertId\n\t}\n\n\tif !dara.IsNil(request.CertName) {\n\t\tquery[\"CertName\"] = request.CertName\n\t}\n\n\tif !dara.IsNil(request.CertRegion) {\n\t\tquery[\"CertRegion\"] = request.CertRegion\n\t}\n\n\tif !dara.IsNil(request.CertType) {\n\t\tquery[\"CertType\"] = request.CertType\n\t}\n\n\tif !dara.IsNil(request.DomainName) {\n\t\tquery[\"DomainName\"] = request.DomainName\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.SSLPri) {\n\t\tquery[\"SSLPri\"] = request.SSLPri\n\t}\n\n\tif !dara.IsNil(request.SSLProtocol) {\n\t\tquery[\"SSLProtocol\"] = request.SSLProtocol\n\t}\n\n\tif !dara.IsNil(request.SSLPub) {\n\t\tquery[\"SSLPub\"] = request.SSLPub\n\t}\n\n\tif !dara.IsNil(request.SecurityToken) {\n\t\tquery[\"SecurityToken\"] = request.SecurityToken\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"SetDcdnDomainSSLCertificate\"),\n\t\tVersion:     dara.String(\"2018-01-15\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alidcdn.SetDcdnDomainSSLCertificateResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-ddospro/aliyun_ddospro.go",
    "content": "package aliyunddospro\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\taliddoscoo \"github.com/alibabacloud-go/ddoscoo-20200101/v5/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-ddospro/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 网站域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.DdoscooClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tResourceGroupId: config.ResourceGroupId,\n\t\tRegion: lo.\n\t\t\tIf(config.Region == \"\" || strings.HasPrefix(config.Region, \"cn-\"), \"cn-hangzhou\").\n\t\t\tElse(\"ap-southeast-1\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain)\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no ddoscoo domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found ddoscoo domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tcertId := upres.ExtendedData[\"CertIdentifier\"].(string)\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, certId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询已配置网站业务转发规则的域名\n\t// REF: https://help.aliyun.com/zh/anti-ddos/anti-ddos-pro-and-premium/developer-reference/api-ddoscoo-2020-01-01-describedomains\n\tdescribeDomainsReq := &aliddoscoo.DescribeDomainsRequest{\n\t\tResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId),\n\t}\n\tdescribeDomainsResp, err := d.sdkClient.DescribeDomainsWithContext(ctx, describeDomainsReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'aliddoscoo.DescribeLiveUserDomains'\", slog.Any(\"request\", describeDomainsReq), slog.Any(\"response\", describeDomainsResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'aliddoscoo.DescribeDomains': %w\", err)\n\t}\n\n\tfor _, domain := range describeDomainsResp.Body.Domains {\n\t\tdomains = append(domains, tea.StringValue(domain))\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error {\n\t// 为网站业务转发规则关联 SSL 证书\n\t// REF: https://help.aliyun.com/zh/anti-ddos/anti-ddos-pro-and-premium/developer-reference/api-ddoscoo-2020-01-01-associatewebcert\n\tassociateWebCertReq := &aliddoscoo.AssociateWebCertRequest{\n\t\tDomain:         tea.String(domain),\n\t\tCertIdentifier: tea.String(cloudCertId),\n\t}\n\tassociateWebCertResp, err := d.sdkClient.AssociateWebCertWithContext(ctx, associateWebCertReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'dcdn.AssociateWebCert'\", slog.Any(\"request\", associateWebCertReq), slog.Any(\"response\", associateWebCertResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'dcdn.AssociateWebCert': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.DdoscooClient, error) {\n\t// 接入点一览 https://api.aliyun.com/product/ddoscoo\n\tvar endpoint string\n\tswitch region {\n\tcase \"\":\n\t\tendpoint = \"ddoscoo.cn-hangzhou.aliyuncs.com\"\n\tdefault:\n\t\tendpoint = fmt.Sprintf(\"ddoscoo.%s.aliyuncs.com\", region)\n\t}\n\n\tconfig := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(endpoint),\n\t}\n\n\tclient, err := internal.NewDdoscooClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-ddospro/aliyun_ddospro_test.go",
    "content": "package aliyunddospro_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-ddospro\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNDDOSPRO_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_ddospro_test.go -args \\\n\t--ALIYUNDDOSPRO_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNDDOSPRO_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNDDOSPRO_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNDDOSPRO_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNDDOSPRO_REGION=\"cn-hangzhou\" \\\n\t--ALIYUNDDOSPRO_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tAccessKeySecret:    fAccessKeySecret,\n\t\t\tRegion:             fRegion,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-ddospro/consts.go",
    "content": "package aliyunddospro\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-ddospro/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\topenapiutil \"github.com/alibabacloud-go/darabonba-openapi/v2/utils\"\n\taliddoscoo \"github.com/alibabacloud-go/ddoscoo-20200101/v5/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/ddoscoo-20200101/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype DdoscooClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewDdoscooClient(config *openapiutil.Config) (*DdoscooClient, error) {\n\tclient := new(DdoscooClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *DdoscooClient) Init(config *openapiutil.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *DdoscooClient) AssociateWebCertWithContext(ctx context.Context, request *aliddoscoo.AssociateWebCertRequest, runtime *dara.RuntimeOptions) (_result *aliddoscoo.AssociateWebCertResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tbody := map[string]interface{}{}\n\n\tif !dara.IsNil(request.Cert) {\n\t\tbody[\"Cert\"] = request.Cert\n\t}\n\n\tif !dara.IsNil(request.CertId) {\n\t\tbody[\"CertId\"] = request.CertId\n\t}\n\n\tif !dara.IsNil(request.CertIdentifier) {\n\t\tbody[\"CertIdentifier\"] = request.CertIdentifier\n\t}\n\n\tif !dara.IsNil(request.CertName) {\n\t\tbody[\"CertName\"] = request.CertName\n\t}\n\n\tif !dara.IsNil(request.CertRegion) {\n\t\tbody[\"CertRegion\"] = request.CertRegion\n\t}\n\n\tif !dara.IsNil(request.Domain) {\n\t\tbody[\"Domain\"] = request.Domain\n\t}\n\n\tif !dara.IsNil(request.Key) {\n\t\tbody[\"Key\"] = request.Key\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tBody: openapiutil.ParseToMap(body),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"AssociateWebCert\"),\n\t\tVersion:     dara.String(\"2020-01-01\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &aliddoscoo.AssociateWebCertResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *DdoscooClient) DescribeDomainsWithContext(ctx context.Context, request *aliddoscoo.DescribeDomainsRequest, runtime *dara.RuntimeOptions) (_result *aliddoscoo.DescribeDomainsResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.InstanceIds) {\n\t\tquery[\"InstanceIds\"] = request.InstanceIds\n\t}\n\n\tif !dara.IsNil(request.ResourceGroupId) {\n\t\tquery[\"ResourceGroupId\"] = request.ResourceGroupId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DescribeDomains\"),\n\t\tVersion:     dara.String(\"2020-01-01\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &aliddoscoo.DescribeDomainsResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go",
    "content": "package aliyunesa\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"strings\"\n\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\taliesa \"github.com/alibabacloud-go/esa-20240910/v2/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-esa/internal\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n\t// 阿里云 ESA 站点 ID。\n\tSiteId int64 `json:\"siteId\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.EsaClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tResourceGroupId: config.ResourceGroupId,\n\t\tRegion: lo.\n\t\t\tIf(config.Region == \"\" || strings.HasPrefix(config.Region, \"cn-\"), \"cn-hangzhou\").\n\t\t\tElse(\"ap-southeast-1\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.SiteId == 0 {\n\t\treturn nil, errors.New(\"config `siteId` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 配置站点证书\n\t// REF: https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-setcertificate\n\tcertId, _ := strconv.ParseInt(upres.CertId, 10, 64)\n\tsetCertificateReq := &aliesa.SetCertificateRequest{\n\t\tSiteId: tea.Int64(d.config.SiteId),\n\t\tType:   tea.String(\"cas\"),\n\t\tCasId:  tea.Int64(certId),\n\t\tRegion: tea.String(d.config.Region),\n\t}\n\tsetCertificateResp, err := d.sdkClient.SetCertificateWithContext(ctx, setCertificateReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'esa.SetCertificate'\", slog.Any(\"request\", setCertificateReq), slog.Any(\"response\", setCertificateResp))\n\tif err != nil {\n\t\tvar sdkError *tea.SDKError\n\t\tif errors.As(err, &sdkError) {\n\t\t\tif tea.StringValue(sdkError.Code) == \"Certificate.Duplicated\" {\n\t\t\t\treturn &deployer.DeployResult{}, nil\n\t\t\t}\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'esa.SetCertificate': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.EsaClient, error) {\n\t// 接入点一览 https://api.aliyun.com/product/ESA\n\tvar endpoint string\n\tswitch region {\n\tcase \"\":\n\t\tendpoint = \"esa.cn-hangzhou.aliyuncs.com\"\n\tdefault:\n\t\tendpoint = fmt.Sprintf(\"esa.%s.aliyuncs.com\", region)\n\t}\n\n\tconfig := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(endpoint),\n\t}\n\n\tclient, err := internal.NewEsaClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-esa/aliyun_esa_test.go",
    "content": "package aliyunesa_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-esa\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfSiteId          int64\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNESA_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.Int64Var(&fSiteId, argsPrefix+\"SITEID\", 0, \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_esa_test.go -args \\\n\t--ALIYUNESA_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNESA_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNESA_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNESA_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNESA_REGION=\"cn-hangzhou\" \\\n\t--ALIYUNESA_SITEID=\"your-esa-site-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"SITEID: %v\", fSiteId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegion:          fRegion,\n\t\t\tSiteId:          fSiteId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-esa/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\topenapiutil \"github.com/alibabacloud-go/darabonba-openapi/v2/utils\"\n\taliesa \"github.com/alibabacloud-go/esa-20240910/v2/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/esa-20240910/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype EsaClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewEsaClient(config *openapiutil.Config) (*EsaClient, error) {\n\tclient := new(EsaClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *EsaClient) Init(config *openapiutil.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *EsaClient) SetCertificateWithContext(ctx context.Context, request *aliesa.SetCertificateRequest, runtime *dara.RuntimeOptions) (_result *aliesa.SetCertificateResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\tif !dara.IsNil(request.KeyServerId) {\n\t\tquery[\"KeyServerId\"] = request.KeyServerId\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.SecurityToken) {\n\t\tquery[\"SecurityToken\"] = request.SecurityToken\n\t}\n\n\tbody := map[string]interface{}{}\n\tif !dara.IsNil(request.CasId) {\n\t\tbody[\"CasId\"] = request.CasId\n\t}\n\n\tif !dara.IsNil(request.Certificate) {\n\t\tbody[\"Certificate\"] = request.Certificate\n\t}\n\n\tif !dara.IsNil(request.Id) {\n\t\tbody[\"Id\"] = request.Id\n\t}\n\n\tif !dara.IsNil(request.Name) {\n\t\tbody[\"Name\"] = request.Name\n\t}\n\n\tif !dara.IsNil(request.PrivateKey) {\n\t\tbody[\"PrivateKey\"] = request.PrivateKey\n\t}\n\n\tif !dara.IsNil(request.Region) {\n\t\tbody[\"Region\"] = request.Region\n\t}\n\n\tif !dara.IsNil(request.SiteId) {\n\t\tbody[\"SiteId\"] = request.SiteId\n\t}\n\n\tif !dara.IsNil(request.Type) {\n\t\tbody[\"Type\"] = request.Type\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t\tBody:  openapiutil.ParseToMap(body),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"SetCertificate\"),\n\t\tVersion:     dara.String(\"2024-09-10\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &aliesa.SetCertificateResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-esa-saas/aliyun_esasaas.go",
    "content": "package aliyunesasaas\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"strings\"\n\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\taliesa \"github.com/alibabacloud-go/esa-20240910/v2/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-esa-saas/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n\t// 阿里云 ESA 站点 ID。\n\tSiteId int64 `json:\"siteId\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// SaaS 域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.EsaClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tResourceGroupId: config.ResourceGroupId,\n\t\tRegion: lo.\n\t\t\tIf(config.Region == \"\" || strings.HasPrefix(config.Region, \"cn-\"), \"cn-hangzhou\").\n\t\t\tElse(\"ap-southeast-1\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.SiteId == 0 {\n\t\treturn nil, errors.New(\"config `siteId` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名 ID 列表\n\tvar hostnameIds []int64\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\thostnameCandidates, err := d.getAllHostnames(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\thostname, ok := lo.Find(hostnameCandidates, func(hostname *aliesa.ListCustomHostnamesResponseBodyHostnames) bool {\n\t\t\t\treturn d.config.Domain == tea.StringValue(hostname.Hostname)\n\t\t\t})\n\t\t\tif !ok {\n\t\t\t\treturn nil, fmt.Errorf(\"could not find hostname '%s'\", d.config.Domain)\n\t\t\t}\n\n\t\t\thostnameIds = []int64{tea.Int64Value(hostname.HostnameId)}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\thostnameCandidates, err := d.getAllHostnames(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\thostnames := lo.Filter(hostnameCandidates, func(hostname *aliesa.ListCustomHostnamesResponseBodyHostnames, _ int) bool {\n\t\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, tea.StringValue(hostname.Hostname))\n\t\t\t\t} else {\n\t\t\t\t\treturn d.config.Domain == tea.StringValue(hostname.Hostname)\n\t\t\t\t}\n\t\t\t})\n\t\t\tif len(hostnames) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any hostnames matched by wildcard\")\n\t\t\t}\n\n\t\t\thostnameIds = lo.Map(hostnames, func(hostname *aliesa.ListCustomHostnamesResponseBodyHostnames, _ int) int64 {\n\t\t\t\treturn tea.Int64Value(hostname.HostnameId)\n\t\t\t})\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\thostnameCandidates, err := d.getAllHostnames(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\thostnames := lo.Filter(hostnameCandidates, func(hostname *aliesa.ListCustomHostnamesResponseBodyHostnames, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(tea.StringValue(hostname.Hostname)) == nil\n\t\t\t})\n\t\t\tif len(hostnames) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any hostnames matched by certificate\")\n\t\t\t}\n\n\t\t\thostnameIds = lo.Map(hostnames, func(hostname *aliesa.ListCustomHostnamesResponseBodyHostnames, _ int) int64 {\n\t\t\t\treturn tea.Int64Value(hostname.HostnameId)\n\t\t\t})\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(hostnameIds) == 0 {\n\t\td.logger.Info(\"no esa saas hostnames to deploy\")\n\t} else {\n\t\td.logger.Info(\"found esa saas hostnames to deploy\", slog.Any(\"hostnameIds\", hostnameIds))\n\t\tvar errs []error\n\n\t\tcertIdentifier := upres.ExtendedData[\"CertIdentifier\"].(string)\n\t\tcertIdentifierSeps := strings.SplitN(certIdentifier, \"-\", 2)\n\t\tif len(certIdentifierSeps) != 2 {\n\t\t\treturn nil, fmt.Errorf(\"received invalid certificate identifier: '%s'\", certIdentifier)\n\t\t}\n\n\t\tcertId, _ := strconv.ParseInt(certIdentifierSeps[0], 10, 64)\n\t\tcertRegion := certIdentifierSeps[1]\n\t\tfor _, hostnameId := range hostnameIds {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateHostnameCertificate(ctx, hostnameId, certId, certRegion); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllHostnames(ctx context.Context) ([]*aliesa.ListCustomHostnamesResponseBodyHostnames, error) {\n\thostnames := make([]*aliesa.ListCustomHostnamesResponseBodyHostnames, 0)\n\n\t// 查询 SaaS 域名列表\n\t// REF: https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-getcustomhostname\n\tlistCustomHostnamesPageNumber := 1\n\tlistCustomHostnamesPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistCustomHostnamesReq := &aliesa.ListCustomHostnamesRequest{\n\t\t\tSiteId:     tea.Int64(d.config.SiteId),\n\t\t\tPageNumber: tea.Int32(int32(listCustomHostnamesPageNumber)),\n\t\t\tPageSize:   tea.Int32(int32(listCustomHostnamesPageSize)),\n\t\t}\n\t\tlistCustomHostnamesResp, err := d.sdkClient.ListCustomHostnamesWithContext(ctx, listCustomHostnamesReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'esa.ListCustomHostnames'\", slog.Any(\"request\", listCustomHostnamesReq), slog.Any(\"response\", listCustomHostnamesResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'esa.ListCustomHostnames': %w\", err)\n\t\t}\n\n\t\tif listCustomHostnamesResp.Body == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tignoredStatuses := []string{\"pending\", \"conflicted\", \"offline\"}\n\t\tfor _, hostnameItem := range listCustomHostnamesResp.Body.Hostnames {\n\t\t\tif lo.Contains(ignoredStatuses, tea.StringValue(hostnameItem.Status)) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\thostnames = append(hostnames, hostnameItem)\n\t\t}\n\n\t\tif len(listCustomHostnamesResp.Body.Hostnames) < listCustomHostnamesPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tlistCustomHostnamesPageNumber++\n\t}\n\n\treturn hostnames, nil\n}\n\nfunc (d *Deployer) updateHostnameCertificate(ctx context.Context, cloudHostnameId int64, cloudCertId int64, cloudCertRegion string) error {\n\t// 更新 SaaS 域名\n\t// REF: https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-updatecustomhostname\n\tupdateCustomHostnameReq := &aliesa.UpdateCustomHostnameRequest{\n\t\tHostnameId: tea.Int64(cloudHostnameId),\n\t\tSslFlag:    tea.String(\"on\"),\n\t\tCertType:   tea.String(\"cas\"),\n\t\tCasId:      tea.Int64(cloudCertId),\n\t\tCasRegion:  tea.String(cloudCertRegion),\n\t}\n\tupdateCustomHostnameResp, err := d.sdkClient.UpdateCustomHostnameWithContext(ctx, updateCustomHostnameReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'esa.UpdateCustomHostname'\", slog.Any(\"request\", updateCustomHostnameReq), slog.Any(\"response\", updateCustomHostnameResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'esa.UpdateCustomHostname': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.EsaClient, error) {\n\t// 接入点一览 https://api.aliyun.com/product/ESA\n\tvar endpoint string\n\tswitch region {\n\tcase \"\":\n\t\tendpoint = \"esa.cn-hangzhou.aliyuncs.com\"\n\tdefault:\n\t\tendpoint = fmt.Sprintf(\"esa.%s.aliyuncs.com\", region)\n\t}\n\n\tconfig := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(endpoint),\n\t}\n\n\tclient, err := internal.NewEsaClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-esa-saas/aliyun_esasaas_test.go",
    "content": "package aliyunesasaas_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-esa-saas\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfSiteId          int64\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNESASAAS_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.Int64Var(&fSiteId, argsPrefix+\"SITEID\", 0, \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_esasaas_test.go -args \\\n\t--ALIYUNESASAAS_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNESASAAS_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNESASAAS_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNESASAAS_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNESASAAS_REGION=\"cn-hangzhou\" \\\n\t--ALIYUNESASAAS_SITEID=\"your-esa-site-id\"\\\n\t--ALIYUNESASAAS_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"SITEID: %v\", fSiteId),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tAccessKeySecret:    fAccessKeySecret,\n\t\t\tRegion:             fRegion,\n\t\t\tSiteId:             fSiteId,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-esa-saas/consts.go",
    "content": "package aliyunesasaas\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-esa-saas/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\topenapiutil \"github.com/alibabacloud-go/darabonba-openapi/v2/utils\"\n\taliesa \"github.com/alibabacloud-go/esa-20240910/v2/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/esa-20240910/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype EsaClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewEsaClient(config *openapiutil.Config) (*EsaClient, error) {\n\tclient := new(EsaClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *EsaClient) Init(config *openapiutil.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *EsaClient) ListCustomHostnamesWithContext(ctx context.Context, request *aliesa.ListCustomHostnamesRequest, runtime *dara.RuntimeOptions) (_result *aliesa.ListCustomHostnamesResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\n\tquery := map[string]interface{}{}\n\tif !dara.IsNil(request.Hostname) {\n\t\tquery[\"Hostname\"] = request.Hostname\n\t}\n\n\tif !dara.IsNil(request.NameMatchType) {\n\t\tquery[\"NameMatchType\"] = request.NameMatchType\n\t}\n\n\tif !dara.IsNil(request.PageNumber) {\n\t\tquery[\"PageNumber\"] = request.PageNumber\n\t}\n\n\tif !dara.IsNil(request.PageSize) {\n\t\tquery[\"PageSize\"] = request.PageSize\n\t}\n\n\tif !dara.IsNil(request.RecordId) {\n\t\tquery[\"RecordId\"] = request.RecordId\n\t}\n\n\tif !dara.IsNil(request.SiteId) {\n\t\tquery[\"SiteId\"] = request.SiteId\n\t}\n\n\tif !dara.IsNil(request.Status) {\n\t\tquery[\"Status\"] = request.Status\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"ListCustomHostnames\"),\n\t\tVersion:     dara.String(\"2024-09-10\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &aliesa.ListCustomHostnamesResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *EsaClient) UpdateCustomHostnameWithContext(ctx context.Context, request *aliesa.UpdateCustomHostnameRequest, runtime *dara.RuntimeOptions) (_result *aliesa.UpdateCustomHostnameResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\n\tquery := map[string]interface{}{}\n\tif !dara.IsNil(request.CasId) {\n\t\tquery[\"CasId\"] = request.CasId\n\t}\n\n\tif !dara.IsNil(request.CasRegion) {\n\t\tquery[\"CasRegion\"] = request.CasRegion\n\t}\n\n\tif !dara.IsNil(request.CertType) {\n\t\tquery[\"CertType\"] = request.CertType\n\t}\n\n\tif !dara.IsNil(request.Certificate) {\n\t\tquery[\"Certificate\"] = request.Certificate\n\t}\n\n\tif !dara.IsNil(request.HostnameId) {\n\t\tquery[\"HostnameId\"] = request.HostnameId\n\t}\n\n\tif !dara.IsNil(request.PrivateKey) {\n\t\tquery[\"PrivateKey\"] = request.PrivateKey\n\t}\n\n\tif !dara.IsNil(request.RecordId) {\n\t\tquery[\"RecordId\"] = request.RecordId\n\t}\n\n\tif !dara.IsNil(request.SslFlag) {\n\t\tquery[\"SslFlag\"] = request.SslFlag\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"UpdateCustomHostname\"),\n\t\tVersion:     dara.String(\"2024-09-10\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &aliesa.UpdateCustomHostnameResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go",
    "content": "package aliyunfc\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\talifc3 \"github.com/alibabacloud-go/fc-20230330/v4/client\"\n\talifc2 \"github.com/alibabacloud-go/fc-open-20210406/v2/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-fc/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n\t// 服务版本。\n\t// 可取值 \"2.0\"、\"3.0\"。\n\tServiceVersion string `json:\"serviceVersion\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 自定义域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClients *wSDKClients\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\ntype wSDKClients struct {\n\tFC2 *internal.FcopenClient\n\tFC3 *internal.FcClient\n}\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclients, err := createSDKClients(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClients: clients,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tswitch d.config.ServiceVersion {\n\tcase \"3\", \"3.0\":\n\t\tif err := d.deployToFC3(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase \"2\", \"2.0\":\n\t\tif err := d.deployToFC2(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported service version '%s'\", d.config.ServiceVersion)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToFC3(ctx context.Context, certPEM, privkeyPEM string) error {\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getFC3AllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain)\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getFC3AllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no fc domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found fc domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateFC3DomainCertificate(ctx, domain, certPEM, privkeyPEM); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToFC2(ctx context.Context, certPEM, privkeyPEM string) error {\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getFC2AllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain)\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getFC2AllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no fc domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found fc domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateFC2DomainCertificate(ctx, domain, certPEM, privkeyPEM); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) getFC3AllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 列出自定义域名\n\t// REF: https://help.aliyun.com/zh/functioncompute/fc/developer-reference/api-fc-2023-03-30-listcustomdomains\n\tlistCustomDomainsNextToken := (*string)(nil)\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistCustomDomainsReq := &alifc3.ListCustomDomainsRequest{\n\t\t\tNextToken: listCustomDomainsNextToken,\n\t\t\tLimit:     tea.Int32(100),\n\t\t}\n\t\tlistCustomDomainsResp, err := d.sdkClients.FC3.ListCustomDomainsWithContext(ctx, listCustomDomainsReq, make(map[string]*string, 0), &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'fc.ListCustomDomains'\", slog.Any(\"request\", listCustomDomainsReq), slog.Any(\"response\", listCustomDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'fc.ListCustomDomains': %w\", err)\n\t\t}\n\n\t\tif listCustomDomainsResp.Body == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, domainItem := range listCustomDomainsResp.Body.CustomDomains {\n\t\t\tdomains = append(domains, tea.StringValue(domainItem.DomainName))\n\t\t}\n\n\t\tif len(listCustomDomainsResp.Body.CustomDomains) == 0 || listCustomDomainsResp.Body.NextToken == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tlistCustomDomainsNextToken = listCustomDomainsResp.Body.NextToken\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) getFC2AllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 列出自定义域名\n\t// REF: https://help.aliyun.com/zh/functioncompute/fc-2-0/developer-reference/api-fc-open-2021-04-06-listcustomdomains\n\tlistCustomDomainsNextToken := (*string)(nil)\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistCustomDomainsReq := &alifc2.ListCustomDomainsRequest{\n\t\t\tNextToken: listCustomDomainsNextToken,\n\t\t\tLimit:     tea.Int32(100),\n\t\t}\n\t\tlistCustomDomainsResp, err := d.sdkClients.FC2.ListCustomDomains(listCustomDomainsReq)\n\t\td.logger.Debug(\"sdk request 'fc.ListCustomDomains'\", slog.Any(\"request\", listCustomDomainsReq), slog.Any(\"response\", listCustomDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'fc.ListCustomDomains': %w\", err)\n\t\t}\n\n\t\tif listCustomDomainsResp.Body == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, domainItem := range listCustomDomainsResp.Body.CustomDomains {\n\t\t\tdomains = append(domains, tea.StringValue(domainItem.DomainName))\n\t\t}\n\n\t\tif len(listCustomDomainsResp.Body.CustomDomains) == 0 || listCustomDomainsResp.Body.NextToken == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tlistCustomDomainsNextToken = listCustomDomainsResp.Body.NextToken\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateFC3DomainCertificate(ctx context.Context, domain string, certPEM, privkeyPEM string) error {\n\t// 获取自定义域名\n\t// REF: https://help.aliyun.com/zh/functioncompute/fc-3-0/developer-reference/api-fc-2023-03-30-getcustomdomain\n\tgetCustomDomainResp, err := d.sdkClients.FC3.GetCustomDomainWithContext(ctx, tea.String(domain), make(map[string]*string), &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'fc.GetCustomDomain'\", slog.Any(\"response\", getCustomDomainResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'fc.GetCustomDomain': %w\", err)\n\t} else {\n\t\tif getCustomDomainResp.Body.CertConfig != nil && tea.StringValue(getCustomDomainResp.Body.CertConfig.Certificate) == certPEM {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// 更新自定义域名\n\t// REF: https://help.aliyun.com/zh/functioncompute/fc-3-0/developer-reference/api-fc-2023-03-30-updatecustomdomain\n\tupdateCustomDomainReq := &alifc3.UpdateCustomDomainRequest{\n\t\tBody: &alifc3.UpdateCustomDomainInput{\n\t\t\tCertConfig: &alifc3.CertConfig{\n\t\t\t\tCertName:    tea.String(fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())),\n\t\t\t\tCertificate: tea.String(certPEM),\n\t\t\t\tPrivateKey:  tea.String(privkeyPEM),\n\t\t\t},\n\t\t\tProtocol:  getCustomDomainResp.Body.Protocol,\n\t\t\tTlsConfig: getCustomDomainResp.Body.TlsConfig,\n\t\t},\n\t}\n\tif tea.StringValue(updateCustomDomainReq.Body.Protocol) == \"HTTP\" {\n\t\tupdateCustomDomainReq.Body.Protocol = tea.String(\"HTTP,HTTPS\")\n\t}\n\tupdateCustomDomainResp, err := d.sdkClients.FC3.UpdateCustomDomainWithContext(ctx, tea.String(domain), updateCustomDomainReq, make(map[string]*string), &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'fc.UpdateCustomDomain'\", slog.Any(\"request\", updateCustomDomainReq), slog.Any(\"response\", updateCustomDomainResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'fc.UpdateCustomDomain': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateFC2DomainCertificate(ctx context.Context, domain string, certPEM, privkeyPEM string) error {\n\t// 获取自定义域名\n\t// REF: https://help.aliyun.com/zh/functioncompute/fc-2-0/developer-reference/api-fc-open-2021-04-06-getcustomdomain\n\tgetCustomDomainResp, err := d.sdkClients.FC2.GetCustomDomain(tea.String(domain))\n\td.logger.Debug(\"sdk request 'fc.GetCustomDomain'\", slog.Any(\"response\", getCustomDomainResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'fc.GetCustomDomain': %w\", err)\n\t} else {\n\t\tif getCustomDomainResp.Body.CertConfig != nil && tea.StringValue(getCustomDomainResp.Body.CertConfig.Certificate) == certPEM {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// 更新自定义域名\n\t// REF: https://help.aliyun.com/zh/functioncompute/fc-2-0/developer-reference/api-fc-open-2021-04-06-updatecustomdomain\n\tupdateCustomDomainReq := &alifc2.UpdateCustomDomainRequest{\n\t\tCertConfig: &alifc2.CertConfig{\n\t\t\tCertName:    tea.String(fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())),\n\t\t\tCertificate: tea.String(certPEM),\n\t\t\tPrivateKey:  tea.String(privkeyPEM),\n\t\t},\n\t\tProtocol:  getCustomDomainResp.Body.Protocol,\n\t\tTlsConfig: getCustomDomainResp.Body.TlsConfig,\n\t}\n\tif tea.StringValue(updateCustomDomainReq.Protocol) == \"HTTP\" {\n\t\tupdateCustomDomainReq.Protocol = tea.String(\"HTTP,HTTPS\")\n\t}\n\tupdateCustomDomainResp, err := d.sdkClients.FC2.UpdateCustomDomain(tea.String(domain), updateCustomDomainReq)\n\td.logger.Debug(\"sdk request 'fc.UpdateCustomDomain'\", slog.Any(\"request\", updateCustomDomainReq), slog.Any(\"response\", updateCustomDomainResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'fc.UpdateCustomDomain': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClients(accessKeyId, accessKeySecret, region string) (*wSDKClients, error) {\n\t// 接入点一览 https://api.aliyun.com/product/FC-Open\n\tvar fc2Endpoint string\n\tswitch region {\n\tcase \"\":\n\t\tfc2Endpoint = \"fc.aliyuncs.com\"\n\tcase \"cn-hangzhou-finance\":\n\t\tfc2Endpoint = fmt.Sprintf(\"%s.fc.aliyuncs.com\", region)\n\tdefault:\n\t\tfc2Endpoint = fmt.Sprintf(\"fc.%s.aliyuncs.com\", region)\n\t}\n\n\tfc2Config := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(fc2Endpoint),\n\t}\n\tfc2Client, err := internal.NewFcopenClient(fc2Config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 接入点一览 https://api.aliyun.com/product/FC\n\tvar fc3Endpoint string\n\tswitch region {\n\tcase \"\":\n\t\tfc3Endpoint = \"fcv3.cn-hangzhou.aliyuncs.com\"\n\tcase \"me-central-1\", \"cn-hangzhou-finance\", \"cn-shanghai-finance-1\", \"cn-heyuan-acdr-1\":\n\t\tfc3Endpoint = fmt.Sprintf(\"%s.fc.aliyuncs.com\", region)\n\tdefault:\n\t\tfc3Endpoint = fmt.Sprintf(\"fcv3.%s.aliyuncs.com\", region)\n\t}\n\n\tfc3Config := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(fc3Endpoint),\n\t}\n\tfc3Client, err := internal.NewFcClient(fc3Config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &wSDKClients{\n\t\tFC2: fc2Client,\n\t\tFC3: fc3Client,\n\t}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-fc/aliyun_fc_test.go",
    "content": "package aliyunfc_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-fc\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNFC_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_fc_test.go -args \\\n\t--ALIYUNFC_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNFC_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNFC_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNFC_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNFC_REGION=\"cn-hangzhou\" \\\n\t--ALIYUNFC_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tAccessKeySecret:    fAccessKeySecret,\n\t\t\tRegion:             fRegion,\n\t\t\tServiceVersion:     \"3.0\",\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-fc/consts.go",
    "content": "package aliyunfc\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-fc/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\topenapiutilv2 \"github.com/alibabacloud-go/darabonba-openapi/v2/utils\"\n\talifc \"github.com/alibabacloud-go/fc-20230330/v4/client\"\n\talifcopen \"github.com/alibabacloud-go/fc-open-20210406/v2/client\"\n\topenapiutil \"github.com/alibabacloud-go/openapi-util/service\"\n\tutil \"github.com/alibabacloud-go/tea-utils/v2/service\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/fc-20230330/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype FcClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewFcClient(config *openapiutilv2.Config) (*FcClient, error) {\n\tclient := new(FcClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *FcClient) Init(config *openapiutilv2.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *FcClient) GetCustomDomainWithContext(ctx context.Context, domainName *string, headers map[string]*string, runtime *dara.RuntimeOptions) (_result *alifc.GetCustomDomainResponse, _err error) {\n\treq := &openapiutilv2.OpenApiRequest{\n\t\tHeaders: headers,\n\t}\n\tparams := &openapiutilv2.Params{\n\t\tAction:      dara.String(\"GetCustomDomain\"),\n\t\tVersion:     dara.String(\"2023-03-30\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/2023-03-30/custom-domains/\" + dara.PercentEncode(dara.StringValue(domainName))),\n\t\tMethod:      dara.String(\"GET\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"ROA\"),\n\t\tReqBodyType: dara.String(\"json\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alifc.GetCustomDomainResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *FcClient) ListCustomDomainsWithContext(ctx context.Context, request *alifc.ListCustomDomainsRequest, headers map[string]*string, runtime *dara.RuntimeOptions) (_result *alifc.ListCustomDomainsResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.Limit) {\n\t\tquery[\"limit\"] = request.Limit\n\t}\n\n\tif !dara.IsNil(request.NextToken) {\n\t\tquery[\"nextToken\"] = request.NextToken\n\t}\n\n\tif !dara.IsNil(request.Prefix) {\n\t\tquery[\"prefix\"] = request.Prefix\n\t}\n\n\treq := &openapiutilv2.OpenApiRequest{\n\t\tHeaders: headers,\n\t\tQuery:   openapiutil.Query(query),\n\t}\n\tparams := &openapiutilv2.Params{\n\t\tAction:      dara.String(\"ListCustomDomains\"),\n\t\tVersion:     dara.String(\"2023-03-30\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/2023-03-30/custom-domains\"),\n\t\tMethod:      dara.String(\"GET\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"ROA\"),\n\t\tReqBodyType: dara.String(\"json\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alifc.ListCustomDomainsResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *FcClient) UpdateCustomDomainWithContext(ctx context.Context, domainName *string, request *alifc.UpdateCustomDomainRequest, headers map[string]*string, runtime *dara.RuntimeOptions) (_result *alifc.UpdateCustomDomainResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\n\treq := &openapiutilv2.OpenApiRequest{\n\t\tHeaders: headers,\n\t\tBody:    openapiutil.ParseToMap(request.Body),\n\t}\n\tparams := &openapiutilv2.Params{\n\t\tAction:      dara.String(\"UpdateCustomDomain\"),\n\t\tVersion:     dara.String(\"2023-03-30\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/2023-03-30/custom-domains/\" + dara.PercentEncode(dara.StringValue(domainName))),\n\t\tMethod:      dara.String(\"PUT\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"ROA\"),\n\t\tReqBodyType: dara.String(\"json\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alifc.UpdateCustomDomainResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\n// This is a partial copy of https://github.com/alibabacloud-go/fc-open-20210406/blob/master/client/client.go\n// to lightweight the vendor packages in the built binary.\ntype FcopenClient struct {\n\topenapi.Client\n}\n\nfunc NewFcopenClient(config *openapi.Config) (*FcopenClient, error) {\n\tclient := new(FcopenClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *FcopenClient) Init(config *openapi.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *FcopenClient) GetCustomDomain(domainName *string) (_result *alifcopen.GetCustomDomainResponse, _err error) {\n\truntime := &util.RuntimeOptions{}\n\theaders := &alifcopen.GetCustomDomainHeaders{}\n\t_result = &alifcopen.GetCustomDomainResponse{}\n\t_body, _err := client.GetCustomDomainWithOptions(domainName, headers, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_result = _body\n\treturn _result, _err\n}\n\nfunc (client *FcopenClient) GetCustomDomainWithOptions(domainName *string, headers *alifcopen.GetCustomDomainHeaders, runtime *util.RuntimeOptions) (_result *alifcopen.GetCustomDomainResponse, _err error) {\n\trealHeaders := make(map[string]*string)\n\n\tif !tea.BoolValue(util.IsUnset(headers.CommonHeaders)) {\n\t\trealHeaders = headers.CommonHeaders\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(headers.XFcAccountId)) {\n\t\trealHeaders[\"X-Fc-Account-Id\"] = util.ToJSONString(headers.XFcAccountId)\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(headers.XFcDate)) {\n\t\trealHeaders[\"X-Fc-Date\"] = util.ToJSONString(headers.XFcDate)\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(headers.XFcTraceId)) {\n\t\trealHeaders[\"X-Fc-Trace-Id\"] = util.ToJSONString(headers.XFcTraceId)\n\t}\n\n\treq := &openapi.OpenApiRequest{\n\t\tHeaders: realHeaders,\n\t}\n\tparams := &openapi.Params{\n\t\tAction:      tea.String(\"GetCustomDomain\"),\n\t\tVersion:     tea.String(\"2021-04-06\"),\n\t\tProtocol:    tea.String(\"HTTPS\"),\n\t\tPathname:    tea.String(\"/2021-04-06/custom-domains/\" + tea.StringValue(openapiutil.GetEncodeParam(domainName))),\n\t\tMethod:      tea.String(\"GET\"),\n\t\tAuthType:    tea.String(\"AK\"),\n\t\tStyle:       tea.String(\"ROA\"),\n\t\tReqBodyType: tea.String(\"json\"),\n\t\tBodyType:    tea.String(\"json\"),\n\t}\n\t_result = &alifcopen.GetCustomDomainResponse{}\n\t_body, _err := client.CallApi(params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = tea.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *FcopenClient) ListCustomDomains(request *alifcopen.ListCustomDomainsRequest) (_result *alifcopen.ListCustomDomainsResponse, _err error) {\n\truntime := &util.RuntimeOptions{}\n\theaders := &alifcopen.ListCustomDomainsHeaders{}\n\t_result = &alifcopen.ListCustomDomainsResponse{}\n\t_body, _err := client.ListCustomDomainsWithOptions(request, headers, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_result = _body\n\treturn _result, _err\n}\n\nfunc (client *FcopenClient) ListCustomDomainsWithOptions(request *alifcopen.ListCustomDomainsRequest, headers *alifcopen.ListCustomDomainsHeaders, runtime *util.RuntimeOptions) (_result *alifcopen.ListCustomDomainsResponse, _err error) {\n\t_err = util.ValidateModel(request)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !tea.BoolValue(util.IsUnset(request.Limit)) {\n\t\tquery[\"limit\"] = request.Limit\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.NextToken)) {\n\t\tquery[\"nextToken\"] = request.NextToken\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.Prefix)) {\n\t\tquery[\"prefix\"] = request.Prefix\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.StartKey)) {\n\t\tquery[\"startKey\"] = request.StartKey\n\t}\n\n\trealHeaders := make(map[string]*string)\n\tif !tea.BoolValue(util.IsUnset(headers.CommonHeaders)) {\n\t\trealHeaders = headers.CommonHeaders\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(headers.XFcAccountId)) {\n\t\trealHeaders[\"X-Fc-Account-Id\"] = util.ToJSONString(headers.XFcAccountId)\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(headers.XFcDate)) {\n\t\trealHeaders[\"X-Fc-Date\"] = util.ToJSONString(headers.XFcDate)\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(headers.XFcTraceId)) {\n\t\trealHeaders[\"X-Fc-Trace-Id\"] = util.ToJSONString(headers.XFcTraceId)\n\t}\n\n\treq := &openapi.OpenApiRequest{\n\t\tHeaders: realHeaders,\n\t\tQuery:   openapiutil.Query(query),\n\t}\n\tparams := &openapi.Params{\n\t\tAction:      tea.String(\"ListCustomDomains\"),\n\t\tVersion:     tea.String(\"2021-04-06\"),\n\t\tProtocol:    tea.String(\"HTTPS\"),\n\t\tPathname:    tea.String(\"/2021-04-06/custom-domains\"),\n\t\tMethod:      tea.String(\"GET\"),\n\t\tAuthType:    tea.String(\"AK\"),\n\t\tStyle:       tea.String(\"ROA\"),\n\t\tReqBodyType: tea.String(\"json\"),\n\t\tBodyType:    tea.String(\"json\"),\n\t}\n\t_result = &alifcopen.ListCustomDomainsResponse{}\n\t_body, _err := client.CallApi(params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = tea.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *FcopenClient) UpdateCustomDomain(domainName *string, request *alifcopen.UpdateCustomDomainRequest) (_result *alifcopen.UpdateCustomDomainResponse, _err error) {\n\truntime := &util.RuntimeOptions{}\n\theaders := &alifcopen.UpdateCustomDomainHeaders{}\n\t_result = &alifcopen.UpdateCustomDomainResponse{}\n\t_body, _err := client.UpdateCustomDomainWithOptions(domainName, request, headers, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_result = _body\n\treturn _result, _err\n}\n\nfunc (client *FcopenClient) UpdateCustomDomainWithOptions(domainName *string, request *alifcopen.UpdateCustomDomainRequest, headers *alifcopen.UpdateCustomDomainHeaders, runtime *util.RuntimeOptions) (_result *alifcopen.UpdateCustomDomainResponse, _err error) {\n\t_err = util.ValidateModel(request)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tbody := map[string]interface{}{}\n\n\tif !tea.BoolValue(util.IsUnset(request.CertConfig)) {\n\t\tbody[\"certConfig\"] = request.CertConfig\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.Protocol)) {\n\t\tbody[\"protocol\"] = request.Protocol\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.RouteConfig)) {\n\t\tbody[\"routeConfig\"] = request.RouteConfig\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.TlsConfig)) {\n\t\tbody[\"tlsConfig\"] = request.TlsConfig\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.WafConfig)) {\n\t\tbody[\"wafConfig\"] = request.WafConfig\n\t}\n\n\trealHeaders := make(map[string]*string)\n\tif !tea.BoolValue(util.IsUnset(headers.CommonHeaders)) {\n\t\trealHeaders = headers.CommonHeaders\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(headers.XFcAccountId)) {\n\t\trealHeaders[\"X-Fc-Account-Id\"] = util.ToJSONString(headers.XFcAccountId)\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(headers.XFcDate)) {\n\t\trealHeaders[\"X-Fc-Date\"] = util.ToJSONString(headers.XFcDate)\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(headers.XFcTraceId)) {\n\t\trealHeaders[\"X-Fc-Trace-Id\"] = util.ToJSONString(headers.XFcTraceId)\n\t}\n\n\treq := &openapi.OpenApiRequest{\n\t\tHeaders: realHeaders,\n\t\tBody:    openapiutil.ParseToMap(body),\n\t}\n\tparams := &openapi.Params{\n\t\tAction:      tea.String(\"UpdateCustomDomain\"),\n\t\tVersion:     tea.String(\"2021-04-06\"),\n\t\tProtocol:    tea.String(\"HTTPS\"),\n\t\tPathname:    tea.String(\"/2021-04-06/custom-domains/\" + tea.StringValue(openapiutil.GetEncodeParam(domainName))),\n\t\tMethod:      tea.String(\"PUT\"),\n\t\tAuthType:    tea.String(\"AK\"),\n\t\tStyle:       tea.String(\"ROA\"),\n\t\tReqBodyType: tea.String(\"json\"),\n\t\tBodyType:    tea.String(\"json\"),\n\t}\n\t_result = &alifcopen.UpdateCustomDomainResponse{}\n\t_body, _err := client.CallApi(params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = tea.Convert(_body, &_result)\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-ga/aliyun_ga.go",
    "content": "package aliyunga\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\taliga \"github.com/alibabacloud-go/ga-20191120/v4/client\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-ga/internal\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 全球加速实例 ID。\n\tAcceleratorId string `json:\"acceleratorId\"`\n\t// 全球加速监听 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。\n\tListenerId string `json:\"listenerId,omitempty\"`\n\t// SNI 域名（不支持泛域名）。\n\t// 部署资源类型为 [RESOURCE_TYPE_ACCELERATOR]、[RESOURCE_TYPE_LISTENER] 时选填。\n\tDomain string `json:\"domain,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.GaClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tResourceGroupId: config.ResourceGroupId,\n\t\tRegion:          \"cn-hangzhou\",\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_ACCELERATOR:\n\t\tif err := d.deployToAccelerator(ctx, upres.ExtendedData[\"CertIdentifier\"].(string)); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_LISTENER:\n\t\tif err := d.deployToListener(ctx, upres.ExtendedData[\"CertIdentifier\"].(string)); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToAccelerator(ctx context.Context, cloudCertId string) error {\n\tif d.config.AcceleratorId == \"\" {\n\t\treturn errors.New(\"config `acceleratorId` is required\")\n\t}\n\n\t// 查询 HTTPS 监听列表\n\t// REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-listlisteners\n\tlistenerIds := make([]string, 0)\n\tlistListenersPageNumber := 1\n\tlistListenersPageSize := 50\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistListenersReq := &aliga.ListListenersRequest{\n\t\t\tRegionId:      tea.String(\"cn-hangzhou\"),\n\t\t\tAcceleratorId: tea.String(d.config.AcceleratorId),\n\t\t\tPageNumber:    tea.Int32(int32(listListenersPageNumber)),\n\t\t\tPageSize:      tea.Int32(int32(listListenersPageSize)),\n\t\t}\n\t\tlistListenersResp, err := d.sdkClient.ListListeners(listListenersReq)\n\t\td.logger.Debug(\"sdk request 'ga.ListListeners'\", slog.Any(\"request\", listListenersReq), slog.Any(\"response\", listListenersResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'ga.ListListeners': %w\", err)\n\t\t}\n\n\t\tif listListenersResp.Body == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, listener := range listListenersResp.Body.Listeners {\n\t\t\tif strings.EqualFold(tea.StringValue(listener.Protocol), \"https\") {\n\t\t\t\tlistenerIds = append(listenerIds, tea.StringValue(listener.ListenerId))\n\t\t\t}\n\t\t}\n\n\t\tif len(listListenersResp.Body.Listeners) < listListenersPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tlistListenersPageNumber++\n\t}\n\n\t// 遍历更新监听证书\n\tif len(listenerIds) == 0 {\n\t\td.logger.Info(\"no ga listeners to deploy\")\n\t} else {\n\t\tvar errs []error\n\t\td.logger.Info(\"found https listeners to deploy\", slog.Any(\"listenerIds\", listenerIds))\n\n\t\tfor _, listenerId := range listenerIds {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateListenerCertificate(ctx, d.config.AcceleratorId, listenerId, cloudCertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error {\n\tif d.config.AcceleratorId == \"\" {\n\t\treturn errors.New(\"config `acceleratorId` is required\")\n\t}\n\tif d.config.ListenerId == \"\" {\n\t\treturn errors.New(\"config `listenerId` is required\")\n\t}\n\n\t// 更新监听\n\tif err := d.updateListenerCertificate(ctx, d.config.AcceleratorId, d.config.ListenerId, cloudCertId); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateListenerCertificate(ctx context.Context, cloudAcceleratorId string, cloudListenerId string, cloudCertId string) error {\n\t// 查询监听绑定的证书列表\n\t// REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-listlistenercertificates\n\tlistenerDefaultCertificate := (*aliga.ListListenerCertificatesResponseBodyCertificates)(nil)\n\tlistenerAdditionalCertificates := make([]*aliga.ListListenerCertificatesResponseBodyCertificates, 0)\n\tlistListenerCertificatesNextToken := (*string)(nil)\n\tfor {\n\t\tlistListenerCertificatesReq := &aliga.ListListenerCertificatesRequest{\n\t\t\tRegionId:      tea.String(\"cn-hangzhou\"),\n\t\t\tAcceleratorId: tea.String(cloudAcceleratorId),\n\t\t\tListenerId:    tea.String(cloudListenerId),\n\t\t\tNextToken:     listListenerCertificatesNextToken,\n\t\t\tMaxResults:    tea.Int32(20),\n\t\t}\n\t\tlistListenerCertificatesResp, err := d.sdkClient.ListListenerCertificates(listListenerCertificatesReq)\n\t\td.logger.Debug(\"sdk request 'ga.ListListenerCertificates'\", slog.Any(\"request\", listListenerCertificatesReq), slog.Any(\"response\", listListenerCertificatesResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'ga.ListListenerCertificates': %w\", err)\n\t\t}\n\n\t\tif listListenerCertificatesResp.Body == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, certItem := range listListenerCertificatesResp.Body.Certificates {\n\t\t\tif tea.BoolValue(certItem.IsDefault) {\n\t\t\t\tlistenerDefaultCertificate = certItem\n\t\t\t} else {\n\t\t\t\tlistenerAdditionalCertificates = append(listenerAdditionalCertificates, certItem)\n\t\t\t}\n\t\t}\n\n\t\tif len(listListenerCertificatesResp.Body.Certificates) == 0 || listListenerCertificatesResp.Body.NextToken == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tlistListenerCertificatesNextToken = listListenerCertificatesResp.Body.NextToken\n\t}\n\n\tif d.config.Domain == \"\" {\n\t\t// 未指定 SNI，只需部署到监听器\n\t\tif listenerDefaultCertificate != nil && tea.StringValue(listenerDefaultCertificate.CertificateId) == cloudCertId {\n\t\t\td.logger.Info(\"no need to update ga listener default certificate\")\n\t\t\treturn nil\n\t\t}\n\n\t\t// 修改监听的属性\n\t\t// REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-updatelistener\n\t\tupdateListenerReq := &aliga.UpdateListenerRequest{\n\t\t\tRegionId:   tea.String(\"cn-hangzhou\"),\n\t\t\tListenerId: tea.String(cloudListenerId),\n\t\t\tCertificates: []*aliga.UpdateListenerRequestCertificates{{\n\t\t\t\tId: tea.String(cloudCertId),\n\t\t\t}},\n\t\t}\n\t\tupdateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq)\n\t\td.logger.Debug(\"sdk request 'ga.UpdateListener'\", slog.Any(\"request\", updateListenerReq), slog.Any(\"response\", updateListenerResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'ga.UpdateListener': %w\", err)\n\t\t}\n\t} else {\n\t\t// 指定 SNI，需部署到扩展域名\n\t\tif lo.SomeBy(listenerAdditionalCertificates, func(item *aliga.ListListenerCertificatesResponseBodyCertificates) bool {\n\t\t\treturn tea.StringValue(item.CertificateId) == cloudCertId\n\t\t}) {\n\t\t\td.logger.Info(\"no need to update ga listener additional certificate\")\n\t\t\treturn nil\n\t\t}\n\n\t\tif lo.SomeBy(listenerAdditionalCertificates, func(item *aliga.ListListenerCertificatesResponseBodyCertificates) bool {\n\t\t\treturn tea.StringValue(item.Domain) == d.config.Domain\n\t\t}) {\n\t\t\t// 为监听替换扩展证书\n\t\t\t// REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-updateadditionalcertificatewithlistener\n\t\t\tupdateAdditionalCertificateWithListenerReq := &aliga.UpdateAdditionalCertificateWithListenerRequest{\n\t\t\t\tRegionId:      tea.String(\"cn-hangzhou\"),\n\t\t\t\tAcceleratorId: tea.String(cloudAcceleratorId),\n\t\t\t\tListenerId:    tea.String(cloudListenerId),\n\t\t\t\tCertificateId: tea.String(cloudCertId),\n\t\t\t\tDomain:        tea.String(d.config.Domain),\n\t\t\t}\n\t\t\tupdateAdditionalCertificateWithListenerResp, err := d.sdkClient.UpdateAdditionalCertificateWithListener(updateAdditionalCertificateWithListenerReq)\n\t\t\td.logger.Debug(\"sdk request 'ga.UpdateAdditionalCertificateWithListener'\", slog.Any(\"request\", updateAdditionalCertificateWithListenerReq), slog.Any(\"response\", updateAdditionalCertificateWithListenerResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'ga.UpdateAdditionalCertificateWithListener': %w\", err)\n\t\t\t}\n\t\t} else {\n\t\t\t// 为监听绑定扩展证书\n\t\t\t// REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-associateadditionalcertificateswithlistener\n\t\t\tassociateAdditionalCertificatesWithListenerReq := &aliga.AssociateAdditionalCertificatesWithListenerRequest{\n\t\t\t\tRegionId:      tea.String(\"cn-hangzhou\"),\n\t\t\t\tAcceleratorId: tea.String(cloudAcceleratorId),\n\t\t\t\tListenerId:    tea.String(cloudListenerId),\n\t\t\t\tCertificates: []*aliga.AssociateAdditionalCertificatesWithListenerRequestCertificates{{\n\t\t\t\t\tId:     tea.String(cloudCertId),\n\t\t\t\t\tDomain: tea.String(d.config.Domain),\n\t\t\t\t}},\n\t\t\t}\n\t\t\tassociateAdditionalCertificatesWithListenerResp, err := d.sdkClient.AssociateAdditionalCertificatesWithListener(associateAdditionalCertificatesWithListenerReq)\n\t\t\td.logger.Debug(\"sdk request 'ga.AssociateAdditionalCertificatesWithListener'\", slog.Any(\"request\", associateAdditionalCertificatesWithListenerReq), slog.Any(\"response\", associateAdditionalCertificatesWithListenerResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'ga.AssociateAdditionalCertificatesWithListener': %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret string) (*internal.GaClient, error) {\n\t// 接入点一览 https://api.aliyun.com/product/Ga\n\tconfig := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(\"ga.cn-hangzhou.aliyuncs.com\"),\n\t}\n\n\tclient, err := internal.NewGaClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-ga/aliyun_ga_test.go",
    "content": "package aliyunga_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-ga\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfAcceleratorId   string\n\tfListenerId      string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNGA_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fAcceleratorId, argsPrefix+\"ACCELERATORID\", \"\", \"\")\n\tflag.StringVar(&fListenerId, argsPrefix+\"LISTENERID\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_ga_test.go -args \\\n\t--ALIYUNGA_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNGA_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNGA_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNGA_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNGA_ACCELERATORID=\"your-ga-accelerator-id\" \\\n\t--ALIYUNGA_LISTENERID=\"your-ga-listener-id\" \\\n\t--ALIYUNGA_DOMAIN=\"your-ga-sni-domain\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy_ToAccelerator\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"ACCELERATORID: %v\", fAcceleratorId),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_ACCELERATOR,\n\t\t\tAcceleratorId:   fAcceleratorId,\n\t\t\tDomain:          fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n\n\tt.Run(\"Deploy_ToListener\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"LISTENERID: %v\", fListenerId),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LISTENER,\n\t\t\tListenerId:      fListenerId,\n\t\t\tDomain:          fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-ga/consts.go",
    "content": "package aliyunga\n\nconst (\n\t// 资源类型：部署到指定全球加速器。\n\tRESOURCE_TYPE_ACCELERATOR = \"accelerator\"\n\t// 资源类型：部署到指定监听器。\n\tRESOURCE_TYPE_LISTENER = \"listener\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-ga/internal/client.go",
    "content": "package internal\n\nimport (\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\taliga \"github.com/alibabacloud-go/ga-20191120/v4/client\"\n\topenapiutil \"github.com/alibabacloud-go/openapi-util/service\"\n\tutil \"github.com/alibabacloud-go/tea-utils/v2/service\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/ga-20191120/blob/master/client/client.go\n// to lightweight the vendor packages in the built binary.\ntype GaClient struct {\n\topenapi.Client\n}\n\nfunc NewGaClient(config *openapi.Config) (*GaClient, error) {\n\tclient := new(GaClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *GaClient) Init(config *openapi.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *GaClient) AssociateAdditionalCertificatesWithListenerWithOptions(request *aliga.AssociateAdditionalCertificatesWithListenerRequest, runtime *util.RuntimeOptions) (_result *aliga.AssociateAdditionalCertificatesWithListenerResponse, _err error) {\n\t_err = util.ValidateModel(request)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !tea.BoolValue(util.IsUnset(request.AcceleratorId)) {\n\t\tquery[\"AcceleratorId\"] = request.AcceleratorId\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.Certificates)) {\n\t\tquery[\"Certificates\"] = request.Certificates\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.ClientToken)) {\n\t\tquery[\"ClientToken\"] = request.ClientToken\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.ListenerId)) {\n\t\tquery[\"ListenerId\"] = request.ListenerId\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.RegionId)) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\treq := &openapi.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapi.Params{\n\t\tAction:      tea.String(\"AssociateAdditionalCertificatesWithListener\"),\n\t\tVersion:     tea.String(\"2019-11-20\"),\n\t\tProtocol:    tea.String(\"HTTPS\"),\n\t\tPathname:    tea.String(\"/\"),\n\t\tMethod:      tea.String(\"POST\"),\n\t\tAuthType:    tea.String(\"AK\"),\n\t\tStyle:       tea.String(\"RPC\"),\n\t\tReqBodyType: tea.String(\"formData\"),\n\t\tBodyType:    tea.String(\"json\"),\n\t}\n\t_result = &aliga.AssociateAdditionalCertificatesWithListenerResponse{}\n\t_body, _err := client.CallApi(params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = tea.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *GaClient) AssociateAdditionalCertificatesWithListener(request *aliga.AssociateAdditionalCertificatesWithListenerRequest) (_result *aliga.AssociateAdditionalCertificatesWithListenerResponse, _err error) {\n\truntime := &util.RuntimeOptions{}\n\t_result = &aliga.AssociateAdditionalCertificatesWithListenerResponse{}\n\t_body, _err := client.AssociateAdditionalCertificatesWithListenerWithOptions(request, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_result = _body\n\treturn _result, _err\n}\n\nfunc (client *GaClient) ListListenersWithOptions(request *aliga.ListListenersRequest, runtime *util.RuntimeOptions) (_result *aliga.ListListenersResponse, _err error) {\n\t_err = util.ValidateModel(request)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !tea.BoolValue(util.IsUnset(request.AcceleratorId)) {\n\t\tquery[\"AcceleratorId\"] = request.AcceleratorId\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.PageNumber)) {\n\t\tquery[\"PageNumber\"] = request.PageNumber\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.PageSize)) {\n\t\tquery[\"PageSize\"] = request.PageSize\n\t}\n\n\tif !dara.IsNil(request.Protocol) {\n\t\tquery[\"Protocol\"] = request.Protocol\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.RegionId)) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\treq := &openapi.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapi.Params{\n\t\tAction:      tea.String(\"ListListeners\"),\n\t\tVersion:     tea.String(\"2019-11-20\"),\n\t\tProtocol:    tea.String(\"HTTPS\"),\n\t\tPathname:    tea.String(\"/\"),\n\t\tMethod:      tea.String(\"POST\"),\n\t\tAuthType:    tea.String(\"AK\"),\n\t\tStyle:       tea.String(\"RPC\"),\n\t\tReqBodyType: tea.String(\"formData\"),\n\t\tBodyType:    tea.String(\"json\"),\n\t}\n\t_result = &aliga.ListListenersResponse{}\n\t_body, _err := client.CallApi(params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = tea.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *GaClient) ListListeners(request *aliga.ListListenersRequest) (_result *aliga.ListListenersResponse, _err error) {\n\truntime := &util.RuntimeOptions{}\n\t_result = &aliga.ListListenersResponse{}\n\t_body, _err := client.ListListenersWithOptions(request, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_result = _body\n\treturn _result, _err\n}\n\nfunc (client *GaClient) ListListenerCertificatesWithOptions(request *aliga.ListListenerCertificatesRequest, runtime *util.RuntimeOptions) (_result *aliga.ListListenerCertificatesResponse, _err error) {\n\t_err = util.ValidateModel(request)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !tea.BoolValue(util.IsUnset(request.AcceleratorId)) {\n\t\tquery[\"AcceleratorId\"] = request.AcceleratorId\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.ListenerId)) {\n\t\tquery[\"ListenerId\"] = request.ListenerId\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.MaxResults)) {\n\t\tquery[\"MaxResults\"] = request.MaxResults\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.NextToken)) {\n\t\tquery[\"NextToken\"] = request.NextToken\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.RegionId)) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.Role)) {\n\t\tquery[\"Role\"] = request.Role\n\t}\n\n\treq := &openapi.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapi.Params{\n\t\tAction:      tea.String(\"ListListenerCertificates\"),\n\t\tVersion:     tea.String(\"2019-11-20\"),\n\t\tProtocol:    tea.String(\"HTTPS\"),\n\t\tPathname:    tea.String(\"/\"),\n\t\tMethod:      tea.String(\"POST\"),\n\t\tAuthType:    tea.String(\"AK\"),\n\t\tStyle:       tea.String(\"RPC\"),\n\t\tReqBodyType: tea.String(\"formData\"),\n\t\tBodyType:    tea.String(\"json\"),\n\t}\n\t_result = &aliga.ListListenerCertificatesResponse{}\n\t_body, _err := client.CallApi(params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = tea.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *GaClient) ListListenerCertificates(request *aliga.ListListenerCertificatesRequest) (_result *aliga.ListListenerCertificatesResponse, _err error) {\n\truntime := &util.RuntimeOptions{}\n\t_result = &aliga.ListListenerCertificatesResponse{}\n\t_body, _err := client.ListListenerCertificatesWithOptions(request, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_result = _body\n\treturn _result, _err\n}\n\nfunc (client *GaClient) UpdateAdditionalCertificateWithListenerWithOptions(request *aliga.UpdateAdditionalCertificateWithListenerRequest, runtime *util.RuntimeOptions) (_result *aliga.UpdateAdditionalCertificateWithListenerResponse, _err error) {\n\t_err = util.ValidateModel(request)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !tea.BoolValue(util.IsUnset(request.AcceleratorId)) {\n\t\tquery[\"AcceleratorId\"] = request.AcceleratorId\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.CertificateId)) {\n\t\tquery[\"CertificateId\"] = request.CertificateId\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.ClientToken)) {\n\t\tquery[\"ClientToken\"] = request.ClientToken\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.Domain)) {\n\t\tquery[\"Domain\"] = request.Domain\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.DryRun)) {\n\t\tquery[\"DryRun\"] = request.DryRun\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.ListenerId)) {\n\t\tquery[\"ListenerId\"] = request.ListenerId\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.RegionId)) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\treq := &openapi.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapi.Params{\n\t\tAction:      tea.String(\"UpdateAdditionalCertificateWithListener\"),\n\t\tVersion:     tea.String(\"2019-11-20\"),\n\t\tProtocol:    tea.String(\"HTTPS\"),\n\t\tPathname:    tea.String(\"/\"),\n\t\tMethod:      tea.String(\"POST\"),\n\t\tAuthType:    tea.String(\"AK\"),\n\t\tStyle:       tea.String(\"RPC\"),\n\t\tReqBodyType: tea.String(\"formData\"),\n\t\tBodyType:    tea.String(\"json\"),\n\t}\n\t_result = &aliga.UpdateAdditionalCertificateWithListenerResponse{}\n\t_body, _err := client.CallApi(params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = tea.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *GaClient) UpdateAdditionalCertificateWithListener(request *aliga.UpdateAdditionalCertificateWithListenerRequest) (_result *aliga.UpdateAdditionalCertificateWithListenerResponse, _err error) {\n\truntime := &util.RuntimeOptions{}\n\t_result = &aliga.UpdateAdditionalCertificateWithListenerResponse{}\n\t_body, _err := client.UpdateAdditionalCertificateWithListenerWithOptions(request, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_result = _body\n\treturn _result, _err\n}\n\nfunc (client *GaClient) UpdateListenerWithOptions(request *aliga.UpdateListenerRequest, runtime *util.RuntimeOptions) (_result *aliga.UpdateListenerResponse, _err error) {\n\t_err = util.ValidateModel(request)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !tea.BoolValue(util.IsUnset(request.BackendPorts)) {\n\t\tquery[\"BackendPorts\"] = request.BackendPorts\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.Certificates)) {\n\t\tquery[\"Certificates\"] = request.Certificates\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.ClientAffinity)) {\n\t\tquery[\"ClientAffinity\"] = request.ClientAffinity\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.ClientToken)) {\n\t\tquery[\"ClientToken\"] = request.ClientToken\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.Description)) {\n\t\tquery[\"Description\"] = request.Description\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.HttpVersion)) {\n\t\tquery[\"HttpVersion\"] = request.HttpVersion\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.IdleTimeout)) {\n\t\tquery[\"IdleTimeout\"] = request.IdleTimeout\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.ListenerId)) {\n\t\tquery[\"ListenerId\"] = request.ListenerId\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.Name)) {\n\t\tquery[\"Name\"] = request.Name\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.PortRanges)) {\n\t\tquery[\"PortRanges\"] = request.PortRanges\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.Protocol)) {\n\t\tquery[\"Protocol\"] = request.Protocol\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.ProxyProtocol)) {\n\t\tquery[\"ProxyProtocol\"] = request.ProxyProtocol\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.RegionId)) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.RequestTimeout)) {\n\t\tquery[\"RequestTimeout\"] = request.RequestTimeout\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.SecurityPolicyId)) {\n\t\tquery[\"SecurityPolicyId\"] = request.SecurityPolicyId\n\t}\n\n\tif !tea.BoolValue(util.IsUnset(request.XForwardedForConfig)) {\n\t\tquery[\"XForwardedForConfig\"] = request.XForwardedForConfig\n\t}\n\n\treq := &openapi.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapi.Params{\n\t\tAction:      tea.String(\"UpdateListener\"),\n\t\tVersion:     tea.String(\"2019-11-20\"),\n\t\tProtocol:    tea.String(\"HTTPS\"),\n\t\tPathname:    tea.String(\"/\"),\n\t\tMethod:      tea.String(\"POST\"),\n\t\tAuthType:    tea.String(\"AK\"),\n\t\tStyle:       tea.String(\"RPC\"),\n\t\tReqBodyType: tea.String(\"formData\"),\n\t\tBodyType:    tea.String(\"json\"),\n\t}\n\t_result = &aliga.UpdateListenerResponse{}\n\t_body, _err := client.CallApi(params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = tea.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *GaClient) UpdateListener(request *aliga.UpdateListenerRequest) (_result *aliga.UpdateListenerResponse, _err error) {\n\truntime := &util.RuntimeOptions{}\n\t_result = &aliga.UpdateListenerResponse{}\n\t_body, _err := client.UpdateListenerWithOptions(request, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_result = _body\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-live/aliyun_live.go",
    "content": "package aliyunlive\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\talilive \"github.com/alibabacloud-go/live-20161101/v2/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-live/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 直播流域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *internal.LiveClient\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\t// \"*.example.com\" → \".example.com\"，适配阿里云 Live 要求的泛域名格式\n\t\t\tdomain := strings.TrimPrefix(d.config.Domain, \"*\")\n\t\t\tdomains = []string{domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain) ||\n\t\t\t\t\t\tstrings.TrimPrefix(d.config.Domain, \"*\") == strings.TrimPrefix(domain, \"*\")\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil ||\n\t\t\t\t\tstrings.TrimPrefix(d.config.Domain, \"*\") == strings.TrimPrefix(domain, \"*\")\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no live domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found live domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, certPEM, privkeyPEM); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询用户名下所有的直播域名\n\t// REF: https://help.aliyun.com/zh/live/developer-reference/api-live-2016-11-01-describeliveuserdomains\n\tdescribeUserLiveDomainsPageNumber := 1\n\tdescribeUserLiveDomainsPageSize := 50\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeUserLiveDomainsReq := &alilive.DescribeLiveUserDomainsRequest{\n\t\t\tResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId),\n\t\t\tRegionName:      tea.String(d.config.Region),\n\t\t\tDomainStatus:    tea.String(\"online\"),\n\t\t\tPageNumber:      tea.Int32(int32(describeUserLiveDomainsPageNumber)),\n\t\t\tPageSize:        tea.Int32(int32(describeUserLiveDomainsPageSize)),\n\t\t}\n\t\tdescribeUserLiveDomainsResp, err := d.sdkClient.DescribeLiveUserDomainsWithContext(ctx, describeUserLiveDomainsReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'live.DescribeLiveUserDomains'\", slog.Any(\"request\", describeUserLiveDomainsReq), slog.Any(\"response\", describeUserLiveDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'live.DescribeLiveUserDomains': %w\", err)\n\t\t}\n\n\t\tif describeUserLiveDomainsResp.Body == nil || describeUserLiveDomainsResp.Body.Domains == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, domainItem := range describeUserLiveDomainsResp.Body.Domains.PageData {\n\t\t\tdomains = append(domains, tea.StringValue(domainItem.DomainName))\n\t\t}\n\n\t\tif len(describeUserLiveDomainsResp.Body.Domains.PageData) < describeUserLiveDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeUserLiveDomainsPageNumber++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, certPEM, privkeyPEM string) error {\n\t// 设置域名证书\n\t// REF: https://help.aliyun.com/zh/live/developer-reference/api-live-2016-11-01-setlivedomaincertificate\n\tsetLiveDomainSSLCertificateReq := &alilive.SetLiveDomainCertificateRequest{\n\t\tDomainName:  tea.String(domain),\n\t\tCertName:    tea.String(fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())),\n\t\tCertType:    tea.String(\"upload\"),\n\t\tSSLProtocol: tea.String(\"on\"),\n\t\tSSLPub:      tea.String(certPEM),\n\t\tSSLPri:      tea.String(privkeyPEM),\n\t}\n\tsetLiveDomainSSLCertificateResp, err := d.sdkClient.SetLiveDomainCertificateWithContext(ctx, setLiveDomainSSLCertificateReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'live.SetLiveDomainCertificate'\", slog.Any(\"request\", setLiveDomainSSLCertificateReq), slog.Any(\"response\", setLiveDomainSSLCertificateResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'live.SetLiveDomainCertificate': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.LiveClient, error) {\n\t// 接入点一览 https://api.aliyun.com/product/live\n\tvar endpoint string\n\tswitch region {\n\tcase \"\",\n\t\t\"cn-qingdao\",\n\t\t\"cn-beijing\",\n\t\t\"cn-shanghai\",\n\t\t\"cn-shenzhen\",\n\t\t\"ap-northeast-1\",\n\t\t\"ap-southeast-5\",\n\t\t\"me-central-1\":\n\t\tendpoint = \"live.aliyuncs.com\"\n\tdefault:\n\t\tendpoint = fmt.Sprintf(\"live.%s.aliyuncs.com\", region)\n\t}\n\n\tconfig := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(endpoint),\n\t}\n\n\tclient, err := internal.NewLiveClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-live/aliyun_live_test.go",
    "content": "package aliyunlive_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-live\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNLIVE_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_live_test.go -args \\\n\t--ALIYUNLIVE_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNLIVE_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNLIVE_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNLIVE_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNLIVE_REGION=\"cn-hangzhou\" \\\n\t--ALIYUNLIVE_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tAccessKeySecret:    fAccessKeySecret,\n\t\t\tRegion:             fRegion,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-live/consts.go",
    "content": "package aliyunlive\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-live/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\topenapiutil \"github.com/alibabacloud-go/darabonba-openapi/v2/utils\"\n\talilive \"github.com/alibabacloud-go/live-20161101/v2/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/live-20161101/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype LiveClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewLiveClient(config *openapiutil.Config) (*LiveClient, error) {\n\tclient := new(LiveClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *LiveClient) Init(config *openapiutil.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *LiveClient) DescribeLiveUserDomainsWithContext(ctx context.Context, request *alilive.DescribeLiveUserDomainsRequest, runtime *dara.RuntimeOptions) (_result *alilive.DescribeLiveUserDomainsResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.DomainName) {\n\t\tquery[\"DomainName\"] = request.DomainName\n\t}\n\n\tif !dara.IsNil(request.DomainSearchType) {\n\t\tquery[\"DomainSearchType\"] = request.DomainSearchType\n\t}\n\n\tif !dara.IsNil(request.DomainStatus) {\n\t\tquery[\"DomainStatus\"] = request.DomainStatus\n\t}\n\n\tif !dara.IsNil(request.LiveDomainType) {\n\t\tquery[\"LiveDomainType\"] = request.LiveDomainType\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.PageNumber) {\n\t\tquery[\"PageNumber\"] = request.PageNumber\n\t}\n\n\tif !dara.IsNil(request.PageSize) {\n\t\tquery[\"PageSize\"] = request.PageSize\n\t}\n\n\tif !dara.IsNil(request.RegionName) {\n\t\tquery[\"RegionName\"] = request.RegionName\n\t}\n\n\tif !dara.IsNil(request.ResourceGroupId) {\n\t\tquery[\"ResourceGroupId\"] = request.ResourceGroupId\n\t}\n\n\tif !dara.IsNil(request.SecurityToken) {\n\t\tquery[\"SecurityToken\"] = request.SecurityToken\n\t}\n\n\tif !dara.IsNil(request.Tag) {\n\t\tquery[\"Tag\"] = request.Tag\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DescribeLiveUserDomains\"),\n\t\tVersion:     dara.String(\"2016-11-01\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alilive.DescribeLiveUserDomainsResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *LiveClient) SetLiveDomainCertificateWithContext(ctx context.Context, request *alilive.SetLiveDomainCertificateRequest, runtime *dara.RuntimeOptions) (_result *alilive.SetLiveDomainCertificateResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.CertName) {\n\t\tquery[\"CertName\"] = request.CertName\n\t}\n\n\tif !dara.IsNil(request.CertType) {\n\t\tquery[\"CertType\"] = request.CertType\n\t}\n\n\tif !dara.IsNil(request.DomainName) {\n\t\tquery[\"DomainName\"] = request.DomainName\n\t}\n\n\tif !dara.IsNil(request.ForceSet) {\n\t\tquery[\"ForceSet\"] = request.ForceSet\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.SSLPri) {\n\t\tquery[\"SSLPri\"] = request.SSLPri\n\t}\n\n\tif !dara.IsNil(request.SSLProtocol) {\n\t\tquery[\"SSLProtocol\"] = request.SSLProtocol\n\t}\n\n\tif !dara.IsNil(request.SSLPub) {\n\t\tquery[\"SSLPub\"] = request.SSLPub\n\t}\n\n\tif !dara.IsNil(request.SecurityToken) {\n\t\tquery[\"SecurityToken\"] = request.SecurityToken\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"SetLiveDomainCertificate\"),\n\t\tVersion:     dara.String(\"2016-11-01\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alilive.SetLiveDomainCertificateResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go",
    "content": "package aliyunnlb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\talinlb \"github.com/alibabacloud-go/nlb-20220430/v4/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-nlb/internal\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 负载均衡实例 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。\n\tLoadbalancerId string `json:\"loadbalancerId,omitempty\"`\n\t// 负载均衡监听 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。\n\tListenerId string `json:\"listenerId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.NlbClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tResourceGroupId: config.ResourceGroupId,\n\t\tRegion: lo.\n\t\t\tIf(config.Region == \"\" || strings.HasPrefix(config.Region, \"cn-\"), \"cn-hangzhou\").\n\t\t\tElse(\"ap-southeast-1\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_LOADBALANCER:\n\t\tif err := d.deployToLoadbalancer(ctx, upres.ExtendedData[\"CertIdentifier\"].(string)); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_LISTENER:\n\t\tif err := d.deployToListener(ctx, upres.ExtendedData[\"CertIdentifier\"].(string)); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\n\t// 查询负载均衡实例的详细信息\n\t// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getloadbalancerattribute\n\tgetLoadBalancerAttributeReq := &alinlb.GetLoadBalancerAttributeRequest{\n\t\tLoadBalancerId: tea.String(d.config.LoadbalancerId),\n\t}\n\tgetLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttributeWithContext(ctx, getLoadBalancerAttributeReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'nlb.GetLoadBalancerAttribute'\", slog.Any(\"request\", getLoadBalancerAttributeReq), slog.Any(\"response\", getLoadBalancerAttributeResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'nlb.GetLoadBalancerAttribute': %w\", err)\n\t}\n\n\t// 查询 TCPSSL 监听列表\n\t// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-listlisteners\n\tlistenerIds := make([]string, 0)\n\tlistListenersToken := (*string)(nil)\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistListenersReq := &alinlb.ListListenersRequest{\n\t\t\tNextToken:        listListenersToken,\n\t\t\tMaxResults:       tea.Int32(100),\n\t\t\tLoadBalancerIds:  tea.StringSlice([]string{d.config.LoadbalancerId}),\n\t\t\tListenerProtocol: tea.String(\"TCPSSL\"),\n\t\t}\n\t\tlistListenersResp, err := d.sdkClient.ListListenersWithContext(ctx, listListenersReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'nlb.ListListeners'\", slog.Any(\"request\", listListenersReq), slog.Any(\"response\", listListenersResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'nlb.ListListeners': %w\", err)\n\t\t}\n\n\t\tif listListenersResp.Body == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, listener := range listListenersResp.Body.Listeners {\n\t\t\tlistenerIds = append(listenerIds, tea.StringValue(listener.ListenerId))\n\t\t}\n\n\t\tif len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tlistListenersToken = listListenersResp.Body.NextToken\n\t}\n\n\t// 遍历更新监听证书\n\tif len(listenerIds) == 0 {\n\t\td.logger.Info(\"no nlb listeners to deploy\")\n\t} else {\n\t\td.logger.Info(\"found tcpssl listeners to deploy\", slog.Any(\"listenerIds\", listenerIds))\n\t\tvar errs []error\n\n\t\tfor _, listenerId := range listenerIds {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error {\n\tif d.config.ListenerId == \"\" {\n\t\treturn errors.New(\"config `listenerId` is required\")\n\t}\n\n\t// 更新监听\n\tif err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {\n\t// 查询监听的属性\n\t// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getlistenerattribute\n\tgetListenerAttributeReq := &alinlb.GetListenerAttributeRequest{\n\t\tListenerId: tea.String(cloudListenerId),\n\t}\n\tgetListenerAttributeResp, err := d.sdkClient.GetListenerAttributeWithContext(ctx, getListenerAttributeReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'nlb.GetListenerAttribute'\", slog.Any(\"request\", getListenerAttributeReq), slog.Any(\"response\", getListenerAttributeResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'nlb.GetListenerAttribute': %w\", err)\n\t}\n\n\t// 修改监听的属性\n\t// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-updatelistenerattribute\n\tupdateListenerAttributeReq := &alinlb.UpdateListenerAttributeRequest{\n\t\tListenerId:     tea.String(cloudListenerId),\n\t\tCertificateIds: []*string{tea.String(cloudCertId)},\n\t}\n\tupdateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttributeWithContext(ctx, updateListenerAttributeReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'nlb.UpdateListenerAttribute'\", slog.Any(\"request\", updateListenerAttributeReq), slog.Any(\"response\", updateListenerAttributeResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'nlb.UpdateListenerAttribute': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.NlbClient, error) {\n\t// 接入点一览 https://api.aliyun.com/product/Nlb\n\tvar endpoint string\n\tswitch region {\n\tcase \"\":\n\t\tendpoint = \"nlb.cn-hangzhou.aliyuncs.com\"\n\tdefault:\n\t\tendpoint = fmt.Sprintf(\"nlb.%s.aliyuncs.com\", region)\n\t}\n\n\tconfig := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(endpoint),\n\t}\n\n\tclient, err := internal.NewNlbClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb_test.go",
    "content": "package aliyunnlb_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-nlb\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfLoadbalancerId  string\n\tfListenerId      string\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNNLB_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fLoadbalancerId, argsPrefix+\"LOADBALANCERID\", \"\", \"\")\n\tflag.StringVar(&fListenerId, argsPrefix+\"LISTENERID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_nlb_test.go -args \\\n\t--ALIYUNNLB_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNNLB_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNNLB_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNNLB_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNNLB_REGION=\"cn-hangzhou\" \\\n\t--ALIYUNNLB_LOADBALANCERID=\"your-nlb-instance-id\" \\\n\t--ALIYUNNLB_LISTENERID=\"your-nlb-listener-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy_ToLoadbalancer\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LOADBALANCERID: %v\", fLoadbalancerId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegion:          fRegion,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LOADBALANCER,\n\t\t\tLoadbalancerId:  fLoadbalancerId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n\n\tt.Run(\"Deploy_ToListener\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LOADBALANCERID: %v\", fLoadbalancerId),\n\t\t\tfmt.Sprintf(\"LISTENERID: %v\", fListenerId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegion:          fRegion,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LISTENER,\n\t\t\tListenerId:      fListenerId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-nlb/consts.go",
    "content": "package aliyunnlb\n\nconst (\n\t// 资源类型：部署到指定负载均衡器。\n\tRESOURCE_TYPE_LOADBALANCER = \"loadbalancer\"\n\t// 资源类型：部署到指定监听器。\n\tRESOURCE_TYPE_LISTENER = \"listener\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-nlb/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\topenapiutil \"github.com/alibabacloud-go/darabonba-openapi/v2/utils\"\n\talinlb \"github.com/alibabacloud-go/nlb-20220430/v4/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/nlb-20220430/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype NlbClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewNlbClient(config *openapiutil.Config) (*NlbClient, error) {\n\tclient := new(NlbClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *NlbClient) Init(config *openapiutil.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *NlbClient) GetListenerAttributeWithContext(ctx context.Context, request *alinlb.GetListenerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alinlb.GetListenerAttributeResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.ClientToken) {\n\t\tquery[\"ClientToken\"] = request.ClientToken\n\t}\n\n\tif !dara.IsNil(request.DryRun) {\n\t\tquery[\"DryRun\"] = request.DryRun\n\t}\n\n\tif !dara.IsNil(request.ListenerId) {\n\t\tquery[\"ListenerId\"] = request.ListenerId\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"GetListenerAttribute\"),\n\t\tVersion:     dara.String(\"2022-04-30\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alinlb.GetListenerAttributeResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *NlbClient) GetLoadBalancerAttributeWithContext(ctx context.Context, request *alinlb.GetLoadBalancerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alinlb.GetLoadBalancerAttributeResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.ClientToken) {\n\t\tquery[\"ClientToken\"] = request.ClientToken\n\t}\n\n\tif !dara.IsNil(request.DryRun) {\n\t\tquery[\"DryRun\"] = request.DryRun\n\t}\n\n\tif !dara.IsNil(request.LoadBalancerId) {\n\t\tquery[\"LoadBalancerId\"] = request.LoadBalancerId\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"GetLoadBalancerAttribute\"),\n\t\tVersion:     dara.String(\"2022-04-30\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alinlb.GetLoadBalancerAttributeResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *NlbClient) ListListenersWithContext(ctx context.Context, request *alinlb.ListListenersRequest, runtime *dara.RuntimeOptions) (_result *alinlb.ListListenersResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.ListenerIds) {\n\t\tquery[\"ListenerIds\"] = request.ListenerIds\n\t}\n\n\tif !dara.IsNil(request.ListenerProtocol) {\n\t\tquery[\"ListenerProtocol\"] = request.ListenerProtocol\n\t}\n\n\tif !dara.IsNil(request.LoadBalancerIds) {\n\t\tquery[\"LoadBalancerIds\"] = request.LoadBalancerIds\n\t}\n\n\tif !dara.IsNil(request.MaxResults) {\n\t\tquery[\"MaxResults\"] = request.MaxResults\n\t}\n\n\tif !dara.IsNil(request.NextToken) {\n\t\tquery[\"NextToken\"] = request.NextToken\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !dara.IsNil(request.SecSensorEnabled) {\n\t\tquery[\"SecSensorEnabled\"] = request.SecSensorEnabled\n\t}\n\n\tif !dara.IsNil(request.Tag) {\n\t\tquery[\"Tag\"] = request.Tag\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"ListListeners\"),\n\t\tVersion:     dara.String(\"2022-04-30\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alinlb.ListListenersResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *NlbClient) UpdateListenerAttributeWithContext(ctx context.Context, tmpReq *alinlb.UpdateListenerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alinlb.UpdateListenerAttributeResponse, _err error) {\n\t_err = tmpReq.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\n\trequest := &alinlb.UpdateListenerAttributeShrinkRequest{}\n\topenapiutil.Convert(tmpReq, request)\n\tif !dara.IsNil(tmpReq.ProxyProtocolV2Config) {\n\t\trequest.ProxyProtocolV2ConfigShrink = openapiutil.ArrayToStringWithSpecifiedStyle(tmpReq.ProxyProtocolV2Config, dara.String(\"ProxyProtocolV2Config\"), dara.String(\"json\"))\n\t}\n\n\tbody := map[string]interface{}{}\n\tif !dara.IsNil(request.AlpnEnabled) {\n\t\tbody[\"AlpnEnabled\"] = request.AlpnEnabled\n\t}\n\n\tif !dara.IsNil(request.AlpnPolicy) {\n\t\tbody[\"AlpnPolicy\"] = request.AlpnPolicy\n\t}\n\n\tif !dara.IsNil(request.CaCertificateIds) {\n\t\tbody[\"CaCertificateIds\"] = request.CaCertificateIds\n\t}\n\n\tif !dara.IsNil(request.CaEnabled) {\n\t\tbody[\"CaEnabled\"] = request.CaEnabled\n\t}\n\n\tif !dara.IsNil(request.CertificateIds) {\n\t\tbody[\"CertificateIds\"] = request.CertificateIds\n\t}\n\n\tif !dara.IsNil(request.ClientToken) {\n\t\tbody[\"ClientToken\"] = request.ClientToken\n\t}\n\n\tif !dara.IsNil(request.Cps) {\n\t\tbody[\"Cps\"] = request.Cps\n\t}\n\n\tif !dara.IsNil(request.DryRun) {\n\t\tbody[\"DryRun\"] = request.DryRun\n\t}\n\n\tif !dara.IsNil(request.IdleTimeout) {\n\t\tbody[\"IdleTimeout\"] = request.IdleTimeout\n\t}\n\n\tif !dara.IsNil(request.ListenerDescription) {\n\t\tbody[\"ListenerDescription\"] = request.ListenerDescription\n\t}\n\n\tif !dara.IsNil(request.ListenerId) {\n\t\tbody[\"ListenerId\"] = request.ListenerId\n\t}\n\n\tif !dara.IsNil(request.Mss) {\n\t\tbody[\"Mss\"] = request.Mss\n\t}\n\n\tif !dara.IsNil(request.ProxyProtocolEnabled) {\n\t\tbody[\"ProxyProtocolEnabled\"] = request.ProxyProtocolEnabled\n\t}\n\n\tif !dara.IsNil(request.ProxyProtocolV2ConfigShrink) {\n\t\tbody[\"ProxyProtocolV2Config\"] = request.ProxyProtocolV2ConfigShrink\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tbody[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !dara.IsNil(request.SecSensorEnabled) {\n\t\tbody[\"SecSensorEnabled\"] = request.SecSensorEnabled\n\t}\n\n\tif !dara.IsNil(request.SecurityPolicyId) {\n\t\tbody[\"SecurityPolicyId\"] = request.SecurityPolicyId\n\t}\n\n\tif !dara.IsNil(request.ServerGroupId) {\n\t\tbody[\"ServerGroupId\"] = request.ServerGroupId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tBody: openapiutil.ParseToMap(body),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"UpdateListenerAttribute\"),\n\t\tVersion:     dara.String(\"2022-04-30\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alinlb.UpdateListenerAttributeResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go",
    "content": "package aliyunoss\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/alibabacloud-go/tea/tea\"\n\t\"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss\"\n\t\"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n\t// 存储桶名。\n\tBucket string `json:\"bucket\"`\n\t// 自定义域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *oss.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.Bucket == \"\" {\n\t\treturn nil, errors.New(\"config `bucket` is required\")\n\t}\n\tif d.config.Domain == \"\" {\n\t\treturn nil, errors.New(\"config `domain` is required\")\n\t}\n\n\t// 为存储空间绑定自定义域名\n\t// REF: https://help.aliyun.com/zh/oss/developer-reference/putcname\n\tputCnameReq := &oss.PutCnameRequest{\n\t\tBucket: tea.String(d.config.Bucket),\n\t\tBucketCnameConfiguration: &oss.BucketCnameConfiguration{\n\t\t\tDomain: tea.String(d.config.Domain),\n\t\t\tCertificateConfiguration: &oss.CertificateConfiguration{\n\t\t\t\tCertificate: tea.String(certPEM),\n\t\t\t\tPrivateKey:  tea.String(privkeyPEM),\n\t\t\t\tForce:       tea.Bool(true),\n\t\t\t},\n\t\t},\n\t}\n\tputCnameResp, err := d.sdkClient.PutCname(ctx, putCnameReq)\n\td.logger.Debug(\"sdk request 'oss.PutCname'\", slog.Any(\"request\", putCnameReq), slog.Any(\"response\", putCnameResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'oss.PutCname': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*oss.Client, error) {\n\t// 接入点一览 https://api.aliyun.com/product/Oss\n\tvar endpoint string\n\tswitch region {\n\tcase \"\":\n\t\tendpoint = \"oss.aliyuncs.com\"\n\tcase\n\t\t\"cn-hzjbp\",\n\t\t\"cn-hzjbp-a\",\n\t\t\"cn-hzjbp-b\":\n\t\tendpoint = \"oss-cn-hzjbp-a-internal.aliyuncs.com\"\n\tcase\n\t\t\"cn-shanghai-finance-1\",\n\t\t\"cn-shenzhen-finance-1\",\n\t\t\"cn-beijing-finance-1\",\n\t\t\"cn-north-2-gov-1\":\n\t\tendpoint = fmt.Sprintf(\"oss-%s-internal.aliyuncs.com\", region)\n\tdefault:\n\t\tendpoint = fmt.Sprintf(\"oss-%s.aliyuncs.com\", region)\n\t}\n\n\tprovider := credentials.NewStaticCredentialsProvider(accessKeyId, accessKeySecret)\n\tconfig := oss.LoadDefaultConfig().\n\t\tWithCredentialsProvider(provider).\n\t\tWithEndpoint(endpoint)\n\tif region != \"\" {\n\t\tconfig = config.WithRegion(region)\n\t}\n\n\tclient := oss.NewClient(config)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-oss/aliyun_oss_test.go",
    "content": "package aliyunoss_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-oss\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfBucket          string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNOSS_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fBucket, argsPrefix+\"BUCKET\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_oss_test.go -args \\\n\t--ALIYUNOSS_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNOSS_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNOSS_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNOSS_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNOSS_REGION=\"cn-hangzhou\" \\\n\t--ALIYUNOSS_BUCKET=\"your-oss-bucket\" \\\n\t--ALIYUNOSS_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"BUCKET: %v\", fBucket),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegion:          fRegion,\n\t\t\tBucket:          fBucket,\n\t\t\tDomain:          fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-vod/aliyun_vod.go",
    "content": "package aliyunvod\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"strings\"\n\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\talivod \"github.com/alibabacloud-go/vod-20170321/v4/client\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-vod/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 点播加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.VodClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tResourceGroupId: config.ResourceGroupId,\n\t\tRegion: lo.\n\t\t\tIf(config.Region == \"\" || strings.HasPrefix(config.Region, \"cn-\"), \"cn-hangzhou\").\n\t\t\tElse(\"ap-southeast-1\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain)\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no vod domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found vod domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tcertIdentifier := upres.ExtendedData[\"CertIdentifier\"].(string)\n\t\tcertIdentifierSeps := strings.SplitN(certIdentifier, \"-\", 2)\n\t\tif len(certIdentifierSeps) != 2 {\n\t\t\treturn nil, fmt.Errorf(\"received invalid certificate identifier: '%s'\", certIdentifier)\n\t\t}\n\n\t\tcertId, _ := strconv.ParseInt(certIdentifierSeps[0], 10, 64)\n\t\tcertName := upres.CertName\n\t\tcertRegion := certIdentifierSeps[1]\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, certId, certName, certRegion); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询加速域名列表\n\t// REF: https://help.aliyun.com/zh/live/developer-reference/api-live-2016-11-01-describeliveuserdomains\n\tdescribeVodUserDomainsPageNumber := 1\n\tdescribeVodUserDomainsPageSize := 50\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeVodUserDomainsReq := &alivod.DescribeVodUserDomainsRequest{\n\t\t\tDomainStatus: tea.String(\"online\"),\n\t\t\tPageNumber:   tea.Int32(int32(describeVodUserDomainsPageNumber)),\n\t\t\tPageSize:     tea.Int32(int32(describeVodUserDomainsPageSize)),\n\t\t}\n\t\tdescribeVodUserDomainsResp, err := d.sdkClient.DescribeVodUserDomainsWithContext(ctx, describeVodUserDomainsReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'vod.DescribeVodUserDomains'\", slog.Any(\"request\", describeVodUserDomainsReq), slog.Any(\"response\", describeVodUserDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'vod.DescribeLiveUserDomains': %w\", err)\n\t\t}\n\n\t\tif describeVodUserDomainsResp.Body == nil || describeVodUserDomainsResp.Body.Domains == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, domainItem := range describeVodUserDomainsResp.Body.Domains.PageData {\n\t\t\tdomains = append(domains, tea.StringValue(domainItem.DomainName))\n\t\t}\n\n\t\tif len(describeVodUserDomainsResp.Body.Domains.PageData) < describeVodUserDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeVodUserDomainsPageNumber++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId int64, cloudCertName, cloudCertRegion string) error {\n\t// 设置域名证书\n\t// REF: https://help.aliyun.com/zh/vod/developer-reference/api-vod-2017-03-21-setvoddomainsslcertificate\n\tsetVodDomainSSLCertificateReq := &alivod.SetVodDomainSSLCertificateRequest{\n\t\tDomainName:  tea.String(domain),\n\t\tCertType:    tea.String(\"cas\"),\n\t\tCertId:      tea.Int64(cloudCertId),\n\t\tCertName:    tea.String(cloudCertName),\n\t\tCertRegion:  tea.String(cloudCertRegion),\n\t\tSSLProtocol: tea.String(\"on\"),\n\t}\n\tsetVodDomainSSLCertificateResp, err := d.sdkClient.SetVodDomainSSLCertificateWithContext(ctx, setVodDomainSSLCertificateReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'live.SetVodDomainSSLCertificate'\", slog.Any(\"request\", setVodDomainSSLCertificateReq), slog.Any(\"response\", setVodDomainSSLCertificateResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'live.SetVodDomainSSLCertificate': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.VodClient, error) {\n\t// 接入点一览 https://api.aliyun.com/product/vod\n\tvar endpoint string\n\tswitch region {\n\tcase \"\":\n\t\tendpoint = \"vod.cn-hangzhou.aliyuncs.com\"\n\tdefault:\n\t\tendpoint = fmt.Sprintf(\"vod.%s.aliyuncs.com\", region)\n\t}\n\n\tconfig := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(endpoint),\n\t}\n\n\tclient, err := internal.NewVodClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-vod/aliyun_vod_test.go",
    "content": "package aliyunvod_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-vod\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNVOD_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_vod_test.go -args \\\n\t--ALIYUNVOD_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNVOD_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNVOD_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNVOD_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNVOD_REGION=\"cn-hangzhou\" \\\n\t--ALIYUNVOD_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tAccessKeySecret:    fAccessKeySecret,\n\t\t\tRegion:             fRegion,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-vod/consts.go",
    "content": "package aliyunvod\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-vod/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\topenapiutil \"github.com/alibabacloud-go/darabonba-openapi/v2/utils\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\talivod \"github.com/alibabacloud-go/vod-20170321/v4/client\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/vod-20170321/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype VodClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewVodClient(config *openapiutil.Config) (*VodClient, error) {\n\tclient := new(VodClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *VodClient) Init(config *openapiutil.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *VodClient) DescribeVodUserDomainsWithContext(ctx context.Context, request *alivod.DescribeVodUserDomainsRequest, runtime *dara.RuntimeOptions) (_result *alivod.DescribeVodUserDomainsResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.DomainName) {\n\t\tquery[\"DomainName\"] = request.DomainName\n\t}\n\n\tif !dara.IsNil(request.DomainSearchType) {\n\t\tquery[\"DomainSearchType\"] = request.DomainSearchType\n\t}\n\n\tif !dara.IsNil(request.DomainStatus) {\n\t\tquery[\"DomainStatus\"] = request.DomainStatus\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.PageNumber) {\n\t\tquery[\"PageNumber\"] = request.PageNumber\n\t}\n\n\tif !dara.IsNil(request.PageSize) {\n\t\tquery[\"PageSize\"] = request.PageSize\n\t}\n\n\tif !dara.IsNil(request.SecurityToken) {\n\t\tquery[\"SecurityToken\"] = request.SecurityToken\n\t}\n\n\tif !dara.IsNil(request.Tag) {\n\t\tquery[\"Tag\"] = request.Tag\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DescribeVodUserDomains\"),\n\t\tVersion:     dara.String(\"2017-03-21\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alivod.DescribeVodUserDomainsResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *VodClient) SetVodDomainSSLCertificateWithContext(ctx context.Context, request *alivod.SetVodDomainSSLCertificateRequest, runtime *dara.RuntimeOptions) (_result *alivod.SetVodDomainSSLCertificateResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.CertId) {\n\t\tquery[\"CertId\"] = request.CertId\n\t}\n\n\tif !dara.IsNil(request.CertName) {\n\t\tquery[\"CertName\"] = request.CertName\n\t}\n\n\tif !dara.IsNil(request.CertRegion) {\n\t\tquery[\"CertRegion\"] = request.CertRegion\n\t}\n\n\tif !dara.IsNil(request.CertType) {\n\t\tquery[\"CertType\"] = request.CertType\n\t}\n\n\tif !dara.IsNil(request.DomainName) {\n\t\tquery[\"DomainName\"] = request.DomainName\n\t}\n\n\tif !dara.IsNil(request.Env) {\n\t\tquery[\"Env\"] = request.Env\n\t}\n\n\tif !dara.IsNil(request.OwnerId) {\n\t\tquery[\"OwnerId\"] = request.OwnerId\n\t}\n\n\tif !dara.IsNil(request.SSLPri) {\n\t\tquery[\"SSLPri\"] = request.SSLPri\n\t}\n\n\tif !dara.IsNil(request.SSLProtocol) {\n\t\tquery[\"SSLProtocol\"] = request.SSLProtocol\n\t}\n\n\tif !dara.IsNil(request.SSLPub) {\n\t\tquery[\"SSLPub\"] = request.SSLPub\n\t}\n\n\tif !dara.IsNil(request.SecurityToken) {\n\t\tquery[\"SecurityToken\"] = request.SecurityToken\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"SetVodDomainSSLCertificate\"),\n\t\tVersion:     dara.String(\"2017-03-21\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &alivod.SetVodDomainSSLCertificateResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go",
    "content": "package aliyunwaf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\taliopen \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/alibabacloud-go/tea/tea\"\n\taliwaf \"github.com/alibabacloud-go/waf-openapi-20211001/v7/client\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-waf/internal\"\n)\n\ntype DeployerConfig struct {\n\t// 阿里云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 阿里云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 阿里云资源组 ID。\n\tResourceGroupId string `json:\"resourceGroupId,omitempty\"`\n\t// 阿里云地域。\n\tRegion string `json:\"region\"`\n\t// 服务版本。\n\t// 可取值 \"3.0\"。\n\tServiceVersion string `json:\"serviceVersion\"`\n\t// 服务类型。\n\tServiceType string `json:\"serviceType\"`\n\t// WAF 实例 ID。\n\tInstanceId string `json:\"instanceId\"`\n\t// 云产品类型。\n\t// 服务类型为 [SERVICE_TYPE_CLOUDRESOURCE] 时必填。\n\tResourceProduct string `json:\"resourceProduct,omitempty\"`\n\t// 云产品资源 ID。\n\t// 服务类型为 [SERVICE_TYPE_CLOUDRESOURCE] 时必填。\n\tResourceId string `json:\"resourceId,omitempty\"`\n\t// 云产品资源端口。\n\t// 服务类型为 [SERVICE_TYPE_CLOUDRESOURCE] 时必填。\n\tResourcePort int32 `json:\"resourcePort,omitempty\"`\n\t// 扩展域名（支持泛域名）。\n\tDomain string `json:\"domain,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.WafClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tResourceGroupId: config.ResourceGroupId,\n\t\tRegion: lo.\n\t\t\tIf(config.Region == \"\" || strings.HasPrefix(config.Region, \"cn-\"), \"cn-hangzhou\").\n\t\t\tElse(\"ap-southeast-1\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tswitch d.config.ServiceVersion {\n\tcase \"3\", \"3.0\":\n\t\tif err := d.deployToWAF3(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported service version '%s'\", d.config.ServiceVersion)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToWAF3(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.InstanceId == \"\" {\n\t\treturn errors.New(\"config `instanceId` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 根据接入方式决定部署方式\n\tswitch d.config.ServiceType {\n\tcase SERVICE_TYPE_CLOUDRESOURCE:\n\t\tcertId := upres.ExtendedData[\"CertIdentifier\"].(string)\n\t\tif err := d.deployToWAF3WithCloudResource(ctx, certId); err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase SERVICE_TYPE_CNAME:\n\t\tcertId := upres.ExtendedData[\"CertIdentifier\"].(string)\n\t\tif err := d.deployToWAF3WithCNAME(ctx, certId); err != nil {\n\t\t\treturn err\n\t\t}\n\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported service version '%s'\", d.config.ServiceVersion)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToWAF3WithCloudResource(ctx context.Context, cloudCertId string) error {\n\tif d.config.ResourceProduct == \"\" {\n\t\treturn errors.New(\"config `resourceProduct` is required\")\n\t}\n\tif d.config.ResourceId == \"\" {\n\t\treturn errors.New(\"config `resourceId` is required\")\n\t}\n\tif d.config.ResourcePort == 0 {\n\t\treturn errors.New(\"config `resourcePort` is required\")\n\t}\n\n\t// 查询云产品实例已同步的证书列表\n\t// REF: https://www.alibabacloud.com/help/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describeresourceinstancecerts\n\t//\n\t// 注意，虽然文档中描述为分页查询，但实际调用不支持分页\n\t// https://github.com/certimate-go/certimate/issues/1122\n\tvar wafResourceInstanceCertificates []*aliwaf.DescribeResourceInstanceCertsResponseBodyCerts\n\tdescribeResourceInstanceCertsReq := &aliwaf.DescribeResourceInstanceCertsRequest{\n\t\tRegionId:                       tea.String(d.config.Region),\n\t\tResourceManagerResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId),\n\t\tInstanceId:                     tea.String(d.config.InstanceId),\n\t\tResourceInstanceId:             tea.String(d.config.ResourceId),\n\t}\n\tdescribeResourceInstanceCertsResp, err := d.sdkClient.DescribeResourceInstanceCertsWithContext(ctx, describeResourceInstanceCertsReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'waf.DescribeResourceInstanceCerts'\", slog.Any(\"request\", describeResourceInstanceCertsReq), slog.Any(\"response\", describeResourceInstanceCertsResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'waf.DescribeResourceInstanceCerts': %w\", err)\n\t} else {\n\t\twafResourceInstanceCertificates = describeResourceInstanceCertsResp.Body.Certs\n\t}\n\n\t// 获取云产品实例的接入端口详情\n\t// REF: https://www.alibabacloud.com/help/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describecloudresourceaccessportdetails\n\tvar wafCloudResourceCloudAccessPort *aliwaf.DescribeCloudResourceAccessPortDetailsResponseBodyAccessPortDetails\n\tvar wafCloudResourceCertificates []*aliwaf.DescribeCloudResourceAccessPortDetailsResponseBodyAccessPortDetailsCertificates\n\tdescribeCloudResourceAccessPortDetailsRequest := &aliwaf.DescribeCloudResourceAccessPortDetailsRequest{\n\t\tRegionId:                       tea.String(d.config.Region),\n\t\tResourceManagerResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId),\n\t\tInstanceId:                     tea.String(d.config.InstanceId),\n\t\tResourceInstanceId:             tea.String(d.config.ResourceId),\n\t\tPort:                           tea.String(fmt.Sprintf(\"%d\", d.config.ResourcePort)),\n\t}\n\tdescribeCloudResourceAccessPortDetailsResponse, err := d.sdkClient.DescribeCloudResourceAccessPortDetailsWithContext(ctx, describeCloudResourceAccessPortDetailsRequest, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'waf.DescribeCloudResourceAccessPortDetails'\", slog.Any(\"request\", describeCloudResourceAccessPortDetailsRequest), slog.Any(\"response\", describeCloudResourceAccessPortDetailsResponse))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'waf.DescribeCloudResourceAccessPortDetails': %w\", err)\n\t} else if len(describeCloudResourceAccessPortDetailsResponse.Body.AccessPortDetails) == 0 {\n\t\treturn fmt.Errorf(\"could not get access port details of waf '%s' cloud resource '%s %s:%d'\", d.config.InstanceId, d.config.ResourceProduct, d.config.ResourceId, d.config.ResourcePort)\n\t} else {\n\t\twafCloudResourceCloudAccessPort = describeCloudResourceAccessPortDetailsResponse.Body.AccessPortDetails[0]\n\t\twafCloudResourceCertificates = wafCloudResourceCloudAccessPort.Certificates\n\t\tif len(wafCloudResourceCertificates) == 0 {\n\t\t\treturn fmt.Errorf(\"could not get access port certificates of waf '%s' cloud resource '%s %s:%d'\", d.config.InstanceId, d.config.ResourceProduct, d.config.ResourceId, d.config.ResourcePort)\n\t\t}\n\t}\n\n\t// 生成请求参数\n\tmodifyCloudResourceCertReq := &aliwaf.ModifyCloudResourceCertRequest{\n\t\tRegionId:        tea.String(d.config.Region),\n\t\tInstanceId:      tea.String(d.config.InstanceId),\n\t\tCloudResourceId: wafCloudResourceCloudAccessPort.CloudResourceId,\n\t}\n\tif d.config.Domain == \"\" {\n\t\t// 未指定扩展域名，只需替换默认证书\n\t\tconst certAppliedTypeDefault = \"default\"\n\n\t\t// 已部署过，直接跳过更新\n\t\tfor _, certItem := range wafCloudResourceCertificates {\n\t\t\tif tea.StringValue(certItem.AppliedType) == certAppliedTypeDefault &&\n\t\t\t\ttea.StringValue(certItem.CertificateId) == cloudCertId {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\t// 移除原默认证书，添加新默认证书\n\t\tmodifyCloudResourceCertReq.Certificates = lo.Map(wafCloudResourceCertificates, func(c *aliwaf.DescribeCloudResourceAccessPortDetailsResponseBodyAccessPortDetailsCertificates, _ int) *aliwaf.ModifyCloudResourceCertRequestCertificates {\n\t\t\treturn &aliwaf.ModifyCloudResourceCertRequestCertificates{\n\t\t\t\tCertificateId: c.CertificateId,\n\t\t\t\tAppliedType:   c.AppliedType,\n\t\t\t}\n\t\t})\n\t\tmodifyCloudResourceCertReq.Certificates = lo.Filter(modifyCloudResourceCertReq.Certificates, func(c *aliwaf.ModifyCloudResourceCertRequestCertificates, _ int) bool {\n\t\t\tif tea.StringValue(c.AppliedType) == certAppliedTypeDefault {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\treturn true\n\t\t})\n\t\tmodifyCloudResourceCertReq.Certificates = append(modifyCloudResourceCertReq.Certificates, &aliwaf.ModifyCloudResourceCertRequestCertificates{\n\t\t\tCertificateId: tea.String(cloudCertId),\n\t\t\tAppliedType:   tea.String(certAppliedTypeDefault),\n\t\t})\n\t} else {\n\t\t// 指定扩展域名，替换或新增扩展证书\n\t\tconst certAppliedTypeExtension = \"extension\"\n\n\t\t// 已部署过，直接跳过更新\n\t\tfor _, certItem := range wafCloudResourceCertificates {\n\t\t\tif tea.StringValue(certItem.AppliedType) == certAppliedTypeExtension &&\n\t\t\t\ttea.StringValue(certItem.CertificateId) == cloudCertId {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\t// 移除同 CommonName 的原扩展证书，添加新扩展证书\n\t\tmodifyCloudResourceCertReq.Certificates = lo.Map(wafCloudResourceCertificates, func(c *aliwaf.DescribeCloudResourceAccessPortDetailsResponseBodyAccessPortDetailsCertificates, _ int) *aliwaf.ModifyCloudResourceCertRequestCertificates {\n\t\t\treturn &aliwaf.ModifyCloudResourceCertRequestCertificates{\n\t\t\t\tCertificateId: c.CertificateId,\n\t\t\t\tAppliedType:   c.AppliedType,\n\t\t\t}\n\t\t})\n\t\tmodifyCloudResourceCertReq.Certificates = lo.Filter(modifyCloudResourceCertReq.Certificates, func(c *aliwaf.ModifyCloudResourceCertRequestCertificates, _ int) bool {\n\t\t\tif tea.StringValue(c.AppliedType) == certAppliedTypeExtension {\n\t\t\t\tif tea.StringValue(c.CertificateId) == cloudCertId {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\n\t\t\t\tvar certCommonName string\n\t\t\t\tfor _, r := range wafResourceInstanceCertificates {\n\t\t\t\t\tif tea.StringValue(c.CertificateId) == tea.StringValue(r.CertIdentifier) {\n\t\t\t\t\t\tcertCommonName = tea.StringValue(r.CommonName)\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif certCommonName == d.config.Domain {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn true\n\t\t})\n\t\tmodifyCloudResourceCertReq.Certificates = append(modifyCloudResourceCertReq.Certificates, &aliwaf.ModifyCloudResourceCertRequestCertificates{\n\t\t\tCertificateId: tea.String(cloudCertId),\n\t\t\tAppliedType:   tea.String(certAppliedTypeExtension),\n\t\t})\n\t}\n\n\t// 过滤掉不存在或已过期的证书，防止接口报错\n\tmodifyCloudResourceCertReq.Certificates = lo.Filter(modifyCloudResourceCertReq.Certificates, func(c *aliwaf.ModifyCloudResourceCertRequestCertificates, _ int) bool {\n\t\tif tea.StringValue(c.CertificateId) == cloudCertId {\n\t\t\treturn true\n\t\t}\n\n\t\tresourceInstanceCert, _ := lo.Find(wafResourceInstanceCertificates, func(r *aliwaf.DescribeResourceInstanceCertsResponseBodyCerts) bool {\n\t\t\tcId := tea.StringValue(c.CertificateId)\n\t\t\trId := tea.StringValue(r.CertIdentifier)\n\t\t\treturn cId == rId || strings.Split(cId, \"-\")[0] == strings.Split(rId, \"-\")[0]\n\t\t})\n\t\tif resourceInstanceCert != nil {\n\t\t\tcertNotAfter := time.Unix(tea.Int64Value(resourceInstanceCert.AfterDate)/1000, 0)\n\t\t\treturn certNotAfter.After(time.Now())\n\t\t}\n\n\t\treturn false\n\t})\n\n\t// 修改云产品接入的证书\n\t// REF: https://www.alibabacloud.com/help/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-modifycloudresourcecert\n\tmodifyCloudResourceCertResp, err := d.sdkClient.ModifyCloudResourceCertWithContext(ctx, modifyCloudResourceCertReq, &dara.RuntimeOptions{})\n\td.logger.Debug(\"sdk request 'waf.ModifyCloudResourceCert'\", slog.Any(\"request\", modifyCloudResourceCertReq), slog.Any(\"response\", modifyCloudResourceCertResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'waf.ModifyCloudResourceCert': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToWAF3WithCNAME(ctx context.Context, cloudCertId string) error {\n\tif d.config.Domain == \"\" {\n\t\t// 未指定扩展域名，只需替换默认证书\n\n\t\t// 查询默认 SSL/TLS 设置\n\t\t// REF: https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describedefaulthttps\n\t\tdescribeDefaultHttpsReq := &aliwaf.DescribeDefaultHttpsRequest{\n\t\t\tResourceManagerResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId),\n\t\t\tRegionId:                       tea.String(d.config.Region),\n\t\t\tInstanceId:                     tea.String(d.config.InstanceId),\n\t\t}\n\t\tdescribeDefaultHttpsResp, err := d.sdkClient.DescribeDefaultHttpsWithContext(ctx, describeDefaultHttpsReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'waf.DescribeDefaultHttps'\", slog.Any(\"request\", describeDefaultHttpsReq), slog.Any(\"response\", describeDefaultHttpsResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'waf.DescribeDefaultHttps': %w\", err)\n\t\t}\n\n\t\t// 修改默认 SSL/TLS 设置\n\t\t// REF: https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-modifydefaulthttps\n\t\tmodifyDefaultHttpsReq := &aliwaf.ModifyDefaultHttpsRequest{\n\t\t\tResourceManagerResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId),\n\t\t\tRegionId:                       tea.String(d.config.Region),\n\t\t\tInstanceId:                     tea.String(d.config.InstanceId),\n\t\t\tCertId:                         tea.String(cloudCertId),\n\t\t\tTLSVersion:                     tea.String(\"tlsv1.2\"),\n\t\t\tEnableTLSv3:                    tea.Bool(true),\n\t\t}\n\t\tif describeDefaultHttpsResp.Body != nil && describeDefaultHttpsResp.Body.DefaultHttps != nil {\n\t\t\tif describeDefaultHttpsResp.Body.DefaultHttps.TLSVersion != nil {\n\t\t\t\tmodifyDefaultHttpsReq.TLSVersion = describeDefaultHttpsResp.Body.DefaultHttps.TLSVersion\n\t\t\t}\n\t\t\tif describeDefaultHttpsResp.Body.DefaultHttps.EnableTLSv3 == nil {\n\t\t\t\tmodifyDefaultHttpsReq.EnableTLSv3 = describeDefaultHttpsResp.Body.DefaultHttps.EnableTLSv3\n\t\t\t}\n\t\t}\n\t\tmodifyDefaultHttpsResp, err := d.sdkClient.ModifyDefaultHttpsWithContext(ctx, modifyDefaultHttpsReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'waf.ModifyDefaultHttps'\", slog.Any(\"request\", modifyDefaultHttpsReq), slog.Any(\"response\", modifyDefaultHttpsResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'waf.ModifyDefaultHttps': %w\", err)\n\t\t}\n\t} else {\n\t\t// 指定扩展域名，需替换扩展证书\n\n\t\t// 查询 CNAME 接入详情\n\t\t// REF: https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describedomaindetail\n\t\tdescribeDomainDetailReq := &aliwaf.DescribeDomainDetailRequest{\n\t\t\tRegionId:   tea.String(d.config.Region),\n\t\t\tInstanceId: tea.String(d.config.InstanceId),\n\t\t\tDomain:     tea.String(d.config.Domain),\n\t\t}\n\t\tdescribeDomainDetailResp, err := d.sdkClient.DescribeDomainDetailWithContext(ctx, describeDomainDetailReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'waf.DescribeDomainDetail'\", slog.Any(\"request\", describeDomainDetailReq), slog.Any(\"response\", describeDomainDetailResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'waf.DescribeDomainDetail': %w\", err)\n\t\t}\n\n\t\t// 修改 CNAME 接入资源\n\t\t// REF: https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-modifydomain\n\t\tmodifyDomainReq := &aliwaf.ModifyDomainRequest{\n\t\t\tRegionId:   tea.String(d.config.Region),\n\t\t\tInstanceId: tea.String(d.config.InstanceId),\n\t\t\tDomain:     tea.String(d.config.Domain),\n\t\t\tListen:     &aliwaf.ModifyDomainRequestListen{CertId: tea.String(cloudCertId)},\n\t\t\tRedirect:   &aliwaf.ModifyDomainRequestRedirect{Loadbalance: tea.String(\"iphash\")},\n\t\t}\n\t\tmodifyDomainReq = _assign(modifyDomainReq, describeDomainDetailResp.Body)\n\t\tmodifyDomainResp, err := d.sdkClient.ModifyDomainWithContext(ctx, modifyDomainReq, &dara.RuntimeOptions{})\n\t\td.logger.Debug(\"sdk request 'waf.ModifyDomain'\", slog.Any(\"request\", modifyDomainReq), slog.Any(\"response\", modifyDomainResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'waf.ModifyDomain': %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.WafClient, error) {\n\t// 接入点一览：https://api.aliyun.com/product/waf-openapi\n\tvar endpoint string\n\tswitch region {\n\tcase \"\":\n\t\tendpoint = \"wafopenapi.cn-hangzhou.aliyuncs.com\"\n\tdefault:\n\t\tendpoint = fmt.Sprintf(\"wafopenapi.%s.aliyuncs.com\", region)\n\t}\n\n\tconfig := &aliopen.Config{\n\t\tAccessKeyId:     tea.String(accessKeyId),\n\t\tAccessKeySecret: tea.String(accessKeySecret),\n\t\tEndpoint:        tea.String(endpoint),\n\t}\n\n\tclient, err := internal.NewWafClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n\nfunc _assign(source *aliwaf.ModifyDomainRequest, target *aliwaf.DescribeDomainDetailResponseBody) *aliwaf.ModifyDomainRequest {\n\t// `ModifyDomain` 中不传的字段表示使用默认值、而非保留原值，\n\t// 因此这里需要把原配置中的参数重新赋值回去。\n\n\tif target == nil {\n\t\treturn source\n\t}\n\n\tif target.Listen != nil {\n\t\tif source.Listen == nil {\n\t\t\tsource.Listen = &aliwaf.ModifyDomainRequestListen{}\n\t\t}\n\n\t\tif target.Listen.CipherSuite != nil {\n\t\t\tsource.Listen.CipherSuite = tea.Int32(int32(*target.Listen.CipherSuite))\n\t\t}\n\n\t\tif target.Listen.CustomCiphers != nil {\n\t\t\tsource.Listen.CustomCiphers = target.Listen.CustomCiphers\n\t\t}\n\n\t\tif target.Listen.EnableTLSv3 != nil {\n\t\t\tsource.Listen.EnableTLSv3 = target.Listen.EnableTLSv3\n\t\t}\n\n\t\tif target.Listen.ExclusiveIp != nil {\n\t\t\tsource.Listen.ExclusiveIp = target.Listen.ExclusiveIp\n\t\t}\n\n\t\tif target.Listen.FocusHttps != nil {\n\t\t\tsource.Listen.FocusHttps = target.Listen.FocusHttps\n\t\t}\n\n\t\tif target.Listen.Http2Enabled != nil {\n\t\t\tsource.Listen.Http2Enabled = target.Listen.Http2Enabled\n\t\t}\n\n\t\tif target.Listen.HttpPorts != nil {\n\t\t\tsource.Listen.HttpPorts = lo.Map(target.Listen.HttpPorts, func(v *int64, _ int) *int32 {\n\t\t\t\tif v == nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn tea.Int32(int32(*v))\n\t\t\t})\n\t\t}\n\n\t\tif target.Listen.HttpsPorts != nil {\n\t\t\tsource.Listen.HttpsPorts = lo.Map(target.Listen.HttpsPorts, func(v *int64, _ int) *int32 {\n\t\t\t\tif v == nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn tea.Int32(int32(*v))\n\t\t\t})\n\t\t}\n\n\t\tif target.Listen.IPv6Enabled != nil {\n\t\t\tsource.Listen.IPv6Enabled = target.Listen.IPv6Enabled\n\t\t}\n\n\t\tif target.Listen.ProtectionResource != nil {\n\t\t\tsource.Listen.ProtectionResource = target.Listen.ProtectionResource\n\t\t}\n\n\t\tif target.Listen.TLSVersion != nil {\n\t\t\tsource.Listen.TLSVersion = target.Listen.TLSVersion\n\t\t}\n\n\t\tif target.Listen.XffHeaderMode != nil {\n\t\t\tsource.Listen.XffHeaderMode = tea.Int32(int32(*target.Listen.XffHeaderMode))\n\t\t}\n\n\t\tif target.Listen.XffHeaders != nil {\n\t\t\tsource.Listen.XffHeaders = target.Listen.XffHeaders\n\t\t}\n\t}\n\n\tif target.Redirect != nil {\n\t\tif source.Redirect == nil {\n\t\t\tsource.Redirect = &aliwaf.ModifyDomainRequestRedirect{}\n\t\t}\n\n\t\tif target.Redirect.Backends != nil {\n\t\t\tsource.Redirect.Backends = lo.Map(target.Redirect.Backends, func(v *aliwaf.DescribeDomainDetailResponseBodyRedirectBackends, _ int) *string {\n\t\t\t\tif v == nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn v.Backend\n\t\t\t})\n\t\t}\n\n\t\tif target.Redirect.BackupBackends != nil {\n\t\t\tsource.Redirect.BackupBackends = lo.Map(target.Redirect.BackupBackends, func(v *aliwaf.DescribeDomainDetailResponseBodyRedirectBackupBackends, _ int) *string {\n\t\t\t\tif v == nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn v.Backend\n\t\t\t})\n\t\t}\n\n\t\tif target.Redirect.ConnectTimeout != nil {\n\t\t\tsource.Redirect.ConnectTimeout = target.Redirect.ConnectTimeout\n\t\t}\n\n\t\tif target.Redirect.FocusHttpBackend != nil {\n\t\t\tsource.Redirect.FocusHttpBackend = target.Redirect.FocusHttpBackend\n\t\t}\n\n\t\tif target.Redirect.Keepalive != nil {\n\t\t\tsource.Redirect.Keepalive = target.Redirect.Keepalive\n\t\t}\n\n\t\tif target.Redirect.KeepaliveRequests != nil {\n\t\t\tsource.Redirect.KeepaliveRequests = target.Redirect.KeepaliveRequests\n\t\t}\n\n\t\tif target.Redirect.KeepaliveTimeout != nil {\n\t\t\tsource.Redirect.KeepaliveTimeout = target.Redirect.KeepaliveTimeout\n\t\t}\n\n\t\tif target.Redirect.Loadbalance != nil {\n\t\t\tsource.Redirect.Loadbalance = target.Redirect.Loadbalance\n\t\t}\n\n\t\tif target.Redirect.ReadTimeout != nil {\n\t\t\tsource.Redirect.ReadTimeout = target.Redirect.ReadTimeout\n\t\t}\n\n\t\tif target.Redirect.RequestHeaders != nil {\n\t\t\tsource.Redirect.RequestHeaders = lo.Map(target.Redirect.RequestHeaders, func(v *aliwaf.DescribeDomainDetailResponseBodyRedirectRequestHeaders, _ int) *aliwaf.ModifyDomainRequestRedirectRequestHeaders {\n\t\t\t\tif v == nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn &aliwaf.ModifyDomainRequestRedirectRequestHeaders{\n\t\t\t\t\tKey:   v.Key,\n\t\t\t\t\tValue: v.Value,\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\n\t\tif target.Redirect.Retry != nil {\n\t\t\tsource.Redirect.Retry = target.Redirect.Retry\n\t\t}\n\n\t\tif target.Redirect.SniEnabled != nil {\n\t\t\tsource.Redirect.SniEnabled = target.Redirect.SniEnabled\n\t\t}\n\n\t\tif target.Redirect.SniHost != nil {\n\t\t\tsource.Redirect.SniHost = target.Redirect.SniHost\n\t\t}\n\n\t\tif target.Redirect.WriteTimeout != nil {\n\t\t\tsource.Redirect.WriteTimeout = target.Redirect.WriteTimeout\n\t\t}\n\n\t\tif target.Redirect.XffProto != nil {\n\t\t\tsource.Redirect.XffProto = target.Redirect.XffProto\n\t\t}\n\t}\n\n\treturn source\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-waf/aliyun_waf_test.go",
    "content": "package aliyunwaf_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-waf\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfInstanceId      string\n)\n\nfunc init() {\n\targsPrefix := \"ALIYUNWAF_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fInstanceId, argsPrefix+\"INSTANCEID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aliyun_waf_test.go -args \\\n\t--ALIYUNWAF_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--ALIYUNWAF_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--ALIYUNWAF_ACCESSKEYID=\"your-access-key-id\" \\\n\t--ALIYUNWAF_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--ALIYUNWAF_REGION=\"cn-hangzhou\" \\\n\t--ALIYUNWAF_INSTANCEID=\"your-waf-instance-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"INSTANCEID: %v\", fInstanceId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegion:          fRegion,\n\t\t\tInstanceId:      fInstanceId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-waf/consts.go",
    "content": "package aliyunwaf\n\nconst (\n\t// 服务类型：云产品接入。\n\tSERVICE_TYPE_CLOUDRESOURCE = \"cloudresource\"\n\t// 服务类型：CNAME 接入。\n\tSERVICE_TYPE_CNAME = \"cname\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/aliyun-waf/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\topenapiutil \"github.com/alibabacloud-go/darabonba-openapi/v2/utils\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\taliwaf \"github.com/alibabacloud-go/waf-openapi-20211001/v7/client\"\n)\n\n// This is a partial copy of https://github.com/alibabacloud-go/waf-openapi-20211001/blob/master/client/client_context_func.go\n// to lightweight the vendor packages in the built binary.\ntype WafClient struct {\n\topenapi.Client\n\tDisableSDKError *bool\n}\n\nfunc NewWafClient(config *openapiutil.Config) (*WafClient, error) {\n\tclient := new(WafClient)\n\terr := client.Init(config)\n\treturn client, err\n}\n\nfunc (client *WafClient) Init(config *openapiutil.Config) (_err error) {\n\t_err = client.Client.Init(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\t_err = client.CheckConfig(config)\n\tif _err != nil {\n\t\treturn _err\n\t}\n\n\treturn nil\n}\n\nfunc (client *WafClient) DescribeCloudResourceAccessPortDetailsWithContext(ctx context.Context, request *aliwaf.DescribeCloudResourceAccessPortDetailsRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.DescribeCloudResourceAccessPortDetailsResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\tif !dara.IsNil(request.InstanceId) {\n\t\tquery[\"InstanceId\"] = request.InstanceId\n\t}\n\n\tif !dara.IsNil(request.PageNumber) {\n\t\tquery[\"PageNumber\"] = request.PageNumber\n\t}\n\n\tif !dara.IsNil(request.PageSize) {\n\t\tquery[\"PageSize\"] = request.PageSize\n\t}\n\n\tif !dara.IsNil(request.Port) {\n\t\tquery[\"Port\"] = request.Port\n\t}\n\n\tif !dara.IsNil(request.Protocol) {\n\t\tquery[\"Protocol\"] = request.Protocol\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !dara.IsNil(request.ResourceInstanceId) {\n\t\tquery[\"ResourceInstanceId\"] = request.ResourceInstanceId\n\t}\n\n\tif !dara.IsNil(request.ResourceManagerResourceGroupId) {\n\t\tquery[\"ResourceManagerResourceGroupId\"] = request.ResourceManagerResourceGroupId\n\t}\n\n\tif !dara.IsNil(request.ResourceProduct) {\n\t\tquery[\"ResourceProduct\"] = request.ResourceProduct\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DescribeCloudResourceAccessPortDetails\"),\n\t\tVersion:     dara.String(\"2021-10-01\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &aliwaf.DescribeCloudResourceAccessPortDetailsResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *WafClient) DescribeDefaultHttpsWithContext(ctx context.Context, request *aliwaf.DescribeDefaultHttpsRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.DescribeDefaultHttpsResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.InstanceId) {\n\t\tquery[\"InstanceId\"] = request.InstanceId\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !dara.IsNil(request.ResourceManagerResourceGroupId) {\n\t\tquery[\"ResourceManagerResourceGroupId\"] = request.ResourceManagerResourceGroupId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DescribeDefaultHttps\"),\n\t\tVersion:     dara.String(\"2021-10-01\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &aliwaf.DescribeDefaultHttpsResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *WafClient) DescribeDomainDetailWithContext(ctx context.Context, request *aliwaf.DescribeDomainDetailRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.DescribeDomainDetailResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.Domain) {\n\t\tquery[\"Domain\"] = request.Domain\n\t}\n\n\tif !dara.IsNil(request.DomainId) {\n\t\tquery[\"DomainId\"] = request.DomainId\n\t}\n\n\tif !dara.IsNil(request.InstanceId) {\n\t\tquery[\"InstanceId\"] = request.InstanceId\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DescribeDomainDetail\"),\n\t\tVersion:     dara.String(\"2021-10-01\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &aliwaf.DescribeDomainDetailResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *WafClient) DescribeResourceInstanceCertsWithContext(ctx context.Context, request *aliwaf.DescribeResourceInstanceCertsRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.DescribeResourceInstanceCertsResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.InstanceId) {\n\t\tquery[\"InstanceId\"] = request.InstanceId\n\t}\n\n\tif !dara.IsNil(request.PageNumber) {\n\t\tquery[\"PageNumber\"] = request.PageNumber\n\t}\n\n\tif !dara.IsNil(request.PageSize) {\n\t\tquery[\"PageSize\"] = request.PageSize\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !dara.IsNil(request.ResourceInstanceId) {\n\t\tquery[\"ResourceInstanceId\"] = request.ResourceInstanceId\n\t}\n\n\tif !dara.IsNil(request.ResourceManagerResourceGroupId) {\n\t\tquery[\"ResourceManagerResourceGroupId\"] = request.ResourceManagerResourceGroupId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"DescribeResourceInstanceCerts\"),\n\t\tVersion:     dara.String(\"2021-10-01\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &aliwaf.DescribeResourceInstanceCertsResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *WafClient) ModifyCloudResourceCertWithContext(ctx context.Context, request *aliwaf.ModifyCloudResourceCertRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.ModifyCloudResourceCertResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\tif !dara.IsNil(request.Certificates) {\n\t\tquery[\"Certificates\"] = request.Certificates\n\t}\n\n\tif !dara.IsNil(request.CloudResourceId) {\n\t\tquery[\"CloudResourceId\"] = request.CloudResourceId\n\t}\n\n\tif !dara.IsNil(request.InstanceId) {\n\t\tquery[\"InstanceId\"] = request.InstanceId\n\t}\n\n\tif !dara.IsNil(request.Port) {\n\t\tquery[\"Port\"] = request.Port\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !dara.IsNil(request.ResourceInstanceId) {\n\t\tquery[\"ResourceInstanceId\"] = request.ResourceInstanceId\n\t}\n\n\tif !dara.IsNil(request.ResourceProduct) {\n\t\tquery[\"ResourceProduct\"] = request.ResourceProduct\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"ModifyCloudResourceCert\"),\n\t\tVersion:     dara.String(\"2021-10-01\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &aliwaf.ModifyCloudResourceCertResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *WafClient) ModifyDefaultHttpsWithContext(ctx context.Context, request *aliwaf.ModifyDefaultHttpsRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.ModifyDefaultHttpsResponse, _err error) {\n\t_err = request.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\tquery := map[string]interface{}{}\n\n\tif !dara.IsNil(request.CertId) {\n\t\tquery[\"CertId\"] = request.CertId\n\t}\n\n\tif !dara.IsNil(request.CipherSuite) {\n\t\tquery[\"CipherSuite\"] = request.CipherSuite\n\t}\n\n\tif !dara.IsNil(request.CustomCiphers) {\n\t\tquery[\"CustomCiphers\"] = request.CustomCiphers\n\t}\n\n\tif !dara.IsNil(request.EnableTLSv3) {\n\t\tquery[\"EnableTLSv3\"] = request.EnableTLSv3\n\t}\n\n\tif !dara.IsNil(request.InstanceId) {\n\t\tquery[\"InstanceId\"] = request.InstanceId\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\tif !dara.IsNil(request.ResourceManagerResourceGroupId) {\n\t\tquery[\"ResourceManagerResourceGroupId\"] = request.ResourceManagerResourceGroupId\n\t}\n\n\tif !dara.IsNil(request.TLSVersion) {\n\t\tquery[\"TLSVersion\"] = request.TLSVersion\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"ModifyDefaultHttps\"),\n\t\tVersion:     dara.String(\"2021-10-01\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &aliwaf.ModifyDefaultHttpsResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n\nfunc (client *WafClient) ModifyDomainWithContext(ctx context.Context, tmpReq *aliwaf.ModifyDomainRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.ModifyDomainResponse, _err error) {\n\t_err = tmpReq.Validate()\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\n\trequest := &aliwaf.ModifyDomainShrinkRequest{}\n\topenapiutil.Convert(tmpReq, request)\n\tif !dara.IsNil(tmpReq.Listen) {\n\t\trequest.ListenShrink = openapiutil.ArrayToStringWithSpecifiedStyle(tmpReq.Listen, dara.String(\"Listen\"), dara.String(\"json\"))\n\t}\n\n\tif !dara.IsNil(tmpReq.Redirect) {\n\t\trequest.RedirectShrink = openapiutil.ArrayToStringWithSpecifiedStyle(tmpReq.Redirect, dara.String(\"Redirect\"), dara.String(\"json\"))\n\t}\n\n\tquery := map[string]interface{}{}\n\tif !dara.IsNil(request.AccessType) {\n\t\tquery[\"AccessType\"] = request.AccessType\n\t}\n\n\tif !dara.IsNil(request.Domain) {\n\t\tquery[\"Domain\"] = request.Domain\n\t}\n\n\tif !dara.IsNil(request.DomainId) {\n\t\tquery[\"DomainId\"] = request.DomainId\n\t}\n\n\tif !dara.IsNil(request.InstanceId) {\n\t\tquery[\"InstanceId\"] = request.InstanceId\n\t}\n\n\tif !dara.IsNil(request.ListenShrink) {\n\t\tquery[\"Listen\"] = request.ListenShrink\n\t}\n\n\tif !dara.IsNil(request.RedirectShrink) {\n\t\tquery[\"Redirect\"] = request.RedirectShrink\n\t}\n\n\tif !dara.IsNil(request.RegionId) {\n\t\tquery[\"RegionId\"] = request.RegionId\n\t}\n\n\treq := &openapiutil.OpenApiRequest{\n\t\tQuery: openapiutil.Query(query),\n\t}\n\tparams := &openapiutil.Params{\n\t\tAction:      dara.String(\"ModifyDomain\"),\n\t\tVersion:     dara.String(\"2021-10-01\"),\n\t\tProtocol:    dara.String(\"HTTPS\"),\n\t\tPathname:    dara.String(\"/\"),\n\t\tMethod:      dara.String(\"POST\"),\n\t\tAuthType:    dara.String(\"AK\"),\n\t\tStyle:       dara.String(\"RPC\"),\n\t\tReqBodyType: dara.String(\"formData\"),\n\t\tBodyType:    dara.String(\"json\"),\n\t}\n\t_result = &aliwaf.ModifyDomainResponse{}\n\t_body, _err := client.CallApiWithCtx(ctx, params, req, runtime)\n\tif _err != nil {\n\t\treturn _result, _err\n\t}\n\t_err = dara.Convert(_body, &_result)\n\treturn _result, _err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/apisix/apisix.go",
    "content": "package apisix\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tapisixsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/apisix\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype DeployerConfig struct {\n\t// APISIX 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// APISIX Admin API Key。\n\tApiKey string `json:\"apiKey\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 证书 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。\n\tCertificateId string `json:\"certificateId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *apisixsdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_CERTIFICATE:\n\t\tif err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.CertificateId == \"\" {\n\t\treturn errors.New(\"config `certificateId` is required\")\n\t}\n\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 更新 SSL 证书\n\t// REF: https://apisix.apache.org/zh/docs/apisix/admin-api/#ssl\n\tsslUpdateReq := &apisixsdk.SslUpdateRequest{\n\t\tID:          lo.ToPtr(d.config.CertificateId),\n\t\tCertificate: lo.ToPtr(certPEM),\n\t\tPrivateKey:  lo.ToPtr(privkeyPEM),\n\t\tSNIs:        lo.ToPtr(certX509.DNSNames),\n\t\tType:        lo.ToPtr(\"server\"),\n\t\tStatus:      lo.ToPtr(int32(1)),\n\t}\n\tsslUpdateResp, err := d.sdkClient.SslUpdateWithContext(ctx, d.config.CertificateId, sslUpdateReq)\n\td.logger.Debug(\"sdk request 'apisix.SslUpdate'\", slog.Any(\"request\", sslUpdateReq), slog.Any(\"response\", sslUpdateResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'apisix.SslUpdate': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*apisixsdk.Client, error) {\n\tclient, err := apisixsdk.NewClient(serverUrl, apiKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/apisix/apisix_test.go",
    "content": "package apisix_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/apisix\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfApiKey        string\n\tfCertificateId string\n)\n\nfunc init() {\n\targsPrefix := \"APISIX_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n\tflag.StringVar(&fCertificateId, argsPrefix+\"CERTIFICATEID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./apisix_test.go -args \\\n\t--APISIX_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--APISIX_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--APISIX_SERVERURL=\"http://127.0.0.1:9080\" \\\n\t--APISIX_APIKEY=\"your-api-key\" \\\n\t--APISIX_CERTIFICATEID=\"your-certificate-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t\tfmt.Sprintf(\"CERTIFICATEID: %v\", fCertificateId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiKey:                   fApiKey,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tResourceType:             provider.RESOURCE_TYPE_CERTIFICATE,\n\t\t\tCertificateId:            fCertificateId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/apisix/consts.go",
    "content": "package apisix\n\nconst (\n\t// 资源类型：替换指定证书。\n\tRESOURCE_TYPE_CERTIFICATE = \"certificate\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/aws-acm/aws_acm.go",
    "content": "package awsacm\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aws-acm\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// AWS AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// AWS SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// AWS 区域。\n\tRegion string `json:\"region\"`\n\t// ACM 证书 ARN。\n\t// 选填。零值时表示新建证书；否则表示更新证书。\n\tCertificateArn string `json:\"certificateArn,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tSecretAccessKey: config.SecretAccessKey,\n\t\tRegion:          config.Region,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.CertificateArn == \"\" {\n\t\t// 上传证书\n\t\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t\t} else {\n\t\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t\t}\n\t} else {\n\t\t// 替换证书\n\t\topres, err := d.sdkCertmgr.Replace(ctx, d.config.CertificateArn, certPEM, privkeyPEM)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to replace certificate file: %w\", err)\n\t\t} else {\n\t\t\td.logger.Info(\"ssl certificate replaced\", slog.Any(\"result\", opres))\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aws-cloudfront/aws_cloudfront.go",
    "content": "package awscloudfront\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\taws \"github.com/aws/aws-sdk-go-v2/aws\"\n\tawscfg \"github.com/aws/aws-sdk-go-v2/config\"\n\tawscred \"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/cloudfront\"\n\t\"github.com/aws/aws-sdk-go-v2/service/cloudfront/types\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgracm \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aws-acm\"\n\tmcertmgriam \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aws-iam\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// AWS AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// AWS SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// AWS 区域。\n\tRegion string `json:\"region\"`\n\t// AWS CloudFront 分配 ID。\n\tDistributionId string `json:\"distributionId\"`\n\t// AWS CloudFront 证书来源。\n\t// 可取值 \"ACM\"、\"IAM\"。\n\tCertificateSource string `json:\"certificateSource\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *cloudfront.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tvar pcertmgr certmgr.Provider\n\tswitch config.CertificateSource {\n\tcase CERTIFICATE_SOURCE_ACM:\n\t\tpcertmgr, err = mcertmgracm.NewCertmgr(&mcertmgracm.CertmgrConfig{\n\t\t\tAccessKeyId:     config.AccessKeyId,\n\t\t\tSecretAccessKey: config.SecretAccessKey,\n\t\t\tRegion:          config.Region,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t\t}\n\n\tcase CERTIFICATE_SOURCE_IAM:\n\t\tpcertmgr, err = mcertmgriam.NewCertmgr(&mcertmgriam.CertmgrConfig{\n\t\t\tAccessKeyId:     config.AccessKeyId,\n\t\t\tSecretAccessKey: config.SecretAccessKey,\n\t\t\tRegion:          config.Region,\n\t\t\tCertificatePath: \"/cloudfront/\",\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported certificate source: '%s'\", config.CertificateSource)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.DistributionId == \"\" {\n\t\treturn nil, errors.New(\"config `distribuitionId` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取分配配置\n\t// REF: https://docs.aws.amazon.com/en_us/cloudfront/latest/APIReference/API_GetDistributionConfig.html\n\tgetDistributionConfigReq := &cloudfront.GetDistributionConfigInput{\n\t\tId: aws.String(d.config.DistributionId),\n\t}\n\tgetDistributionConfigResp, err := d.sdkClient.GetDistributionConfig(ctx, getDistributionConfigReq)\n\td.logger.Debug(\"sdk request 'cloudfront.GetDistributionConfig'\", slog.Any(\"request\", getDistributionConfigReq), slog.Any(\"response\", getDistributionConfigResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cloudfront.GetDistributionConfig': %w\", err)\n\t}\n\n\t// 更新分配配置\n\t// REF: https://docs.aws.amazon.com/zh_cn/cloudfront/latest/APIReference/API_UpdateDistribution.html\n\tupdateDistributionReq := &cloudfront.UpdateDistributionInput{\n\t\tId:                 aws.String(d.config.DistributionId),\n\t\tDistributionConfig: getDistributionConfigResp.DistributionConfig,\n\t\tIfMatch:            getDistributionConfigResp.ETag,\n\t}\n\tif updateDistributionReq.DistributionConfig.ViewerCertificate == nil {\n\t\tupdateDistributionReq.DistributionConfig.ViewerCertificate = &types.ViewerCertificate{}\n\t}\n\tupdateDistributionReq.DistributionConfig.ViewerCertificate.CloudFrontDefaultCertificate = aws.Bool(false)\n\tswitch d.config.CertificateSource {\n\tcase CERTIFICATE_SOURCE_ACM:\n\t\tupdateDistributionReq.DistributionConfig.ViewerCertificate.ACMCertificateArn = aws.String(upres.CertId)\n\t\tupdateDistributionReq.DistributionConfig.ViewerCertificate.IAMCertificateId = nil\n\n\tcase CERTIFICATE_SOURCE_IAM:\n\t\tupdateDistributionReq.DistributionConfig.ViewerCertificate.ACMCertificateArn = nil\n\t\tupdateDistributionReq.DistributionConfig.ViewerCertificate.IAMCertificateId = aws.String(upres.CertId)\n\t\tif updateDistributionReq.DistributionConfig.ViewerCertificate.MinimumProtocolVersion == \"\" {\n\t\t\tupdateDistributionReq.DistributionConfig.ViewerCertificate.MinimumProtocolVersion = types.MinimumProtocolVersionTLSv122018\n\t\t}\n\t\tif updateDistributionReq.DistributionConfig.ViewerCertificate.SSLSupportMethod == \"\" {\n\t\t\tupdateDistributionReq.DistributionConfig.ViewerCertificate.SSLSupportMethod = types.SSLSupportMethodSniOnly\n\t\t}\n\t}\n\tupdateDistributionResp, err := d.sdkClient.UpdateDistribution(ctx, updateDistributionReq)\n\td.logger.Debug(\"sdk request 'cloudfront.UpdateDistribution'\", slog.Any(\"request\", updateDistributionReq), slog.Any(\"response\", updateDistributionResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cloudfront.UpdateDistribution': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey, region string) (*cloudfront.Client, error) {\n\tcfg, err := awscfg.LoadDefaultConfig(context.Background())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := cloudfront.NewFromConfig(cfg, func(o *cloudfront.Options) {\n\t\to.Region = region\n\t\to.Credentials = aws.NewCredentialsCache(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, \"\"))\n\t})\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aws-cloudfront/aws_cloudfront_test.go",
    "content": "package awscloudfront_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/aws-cloudfront\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n\tfRegion          string\n\tfDistribuitionId string\n)\n\nfunc init() {\n\targsPrefix := \"AWSCLOUDFRONT_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fDistribuitionId, argsPrefix+\"DISTRIBUTIONID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./aws_cloudfront_test.go -args \\\n\t--AWSCLOUDFRONT_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--AWSCLOUDFRONT_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--AWSCLOUDFRONT_ACCESSKEYID=\"your-access-key-id\" \\\n\t--AWSCLOUDFRONT_SECRETACCESSKEY=\"your-secret-access-id\" \\\n\t--AWSCLOUDFRONT_REGION=\"us-east-1\" \\\n\t--AWSCLOUDFRONT_DISTRIBUTIONID=\"your-distribution-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"DISTRIBUTIONID: %v\", fDistribuitionId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t\tRegion:          fRegion,\n\t\t\tDistributionId:  fDistribuitionId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/aws-cloudfront/consts.go",
    "content": "package awscloudfront\n\nconst (\n\tCERTIFICATE_SOURCE_ACM = \"ACM\"\n\tCERTIFICATE_SOURCE_IAM = \"IAM\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/aws-iam/aws_iam.go",
    "content": "package awsiam\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/aws-iam\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// AWS AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// AWS SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// AWS 区域。\n\tRegion string `json:\"region\"`\n\t// IAM 证书路径。\n\t// 选填。\n\tCertificatePath string `json:\"certificatePath,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tSecretAccessKey: config.SecretAccessKey,\n\t\tRegion:          config.Region,\n\t\tCertificatePath: config.CertificatePath,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go",
    "content": "package azurekeyvault\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/azure-keyvault\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// Azure TenantId。\n\tTenantId string `json:\"tenantId\"`\n\t// Azure ClientId。\n\tClientId string `json:\"clientId\"`\n\t// Azure ClientSecret。\n\tClientSecret string `json:\"clientSecret\"`\n\t// Azure 主权云环境。\n\tCloudName string `json:\"cloudName,omitempty\"`\n\t// Key Vault 名称。\n\tKeyVaultName string `json:\"keyvaultName\"`\n\t// Key Vault 证书名称。\n\t// 选填。零值时表示新建证书；否则表示更新证书。\n\tCertificateName string `json:\"certificateName,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tTenantId:     config.TenantId,\n\t\tClientId:     config.ClientId,\n\t\tClientSecret: config.ClientSecret,\n\t\tCloudName:    config.CloudName,\n\t\tKeyVaultName: config.KeyVaultName,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.CertificateName == \"\" {\n\t\t// 上传证书\n\t\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t\t} else {\n\t\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t\t}\n\t} else {\n\t\t// 替换证书\n\t\topres, err := d.sdkCertmgr.Replace(ctx, d.config.CertificateName, certPEM, privkeyPEM)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to replace certificate file: %w\", err)\n\t\t} else {\n\t\t\td.logger.Info(\"ssl certificate replaced\", slog.Any(\"result\", opres))\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go",
    "content": "package baiducloudappblb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"strings\"\n\n\tbceappblb \"github.com/baidubce/bce-sdk-go/services/appblb\"\n\t\"github.com/pocketbase/pocketbase/tools/security\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/baiducloud-cert\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// 百度智能云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 百度智能云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 百度智能云区域。\n\tRegion string `json:\"region\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 负载均衡实例 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时必填。\n\tLoadbalancerId string `json:\"loadbalancerId,omitempty\"`\n\t// 负载均衡监听端口。\n\t// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。\n\tListenerPort int32 `json:\"listenerPort,omitempty\"`\n\t// SNI 域名（支持泛域名）。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。\n\tDomain string `json:\"domain,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *bceappblb.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tSecretAccessKey: config.SecretAccessKey,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_LOADBALANCER:\n\t\tif err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_LISTENER:\n\t\tif err := d.deployToListener(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\n\t// 查询 BLB 实例详情\n\t// REF: https://cloud.baidu.com/doc/BLB/s/6jwvxnyhi#describeloadbalancerdetail%E6%9F%A5%E8%AF%A2blb%E5%AE%9E%E4%BE%8B%E8%AF%A6%E6%83%85\n\tdescribeLoadBalancerDetailResp, err := d.sdkClient.DescribeLoadBalancerDetail(d.config.LoadbalancerId)\n\td.logger.Debug(\"sdk request 'appblb.DescribeLoadBalancerAttribute'\", slog.String(\"blbId\", d.config.LoadbalancerId), slog.Any(\"response\", describeLoadBalancerDetailResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'appblb.DescribeLoadBalancerDetail': %w\", err)\n\t}\n\n\t// 获取全部 HTTPS/SSL 监听端口\n\tlisteners := make([]struct {\n\t\tType string\n\t\tPort int32\n\t}, 0)\n\tfor _, listener := range describeLoadBalancerDetailResp.Listener {\n\t\tif listener.Type == \"HTTPS\" || listener.Type == \"SSL\" {\n\t\t\tlistenerPort, err := strconv.Atoi(listener.Port)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlisteners = append(listeners, struct {\n\t\t\t\tType string\n\t\t\t\tPort int32\n\t\t\t}{\n\t\t\t\tType: listener.Type,\n\t\t\t\tPort: int32(listenerPort),\n\t\t\t})\n\t\t}\n\t}\n\n\t// 遍历更新监听证书\n\tif len(listeners) == 0 {\n\t\td.logger.Info(\"no blb listeners to deploy\")\n\t} else {\n\t\td.logger.Info(\"found https/ssl listeners to deploy\", slog.Any(\"listeners\", listeners))\n\t\tvar errs []error\n\n\t\tfor _, listener := range listeners {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\n\t\t\tdefault:\n\t\t\t\tif err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\tif d.config.ListenerPort == 0 {\n\t\treturn errors.New(\"config `listenerPort` is required\")\n\t}\n\n\t// 查询监听\n\t// REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#describeappalllisteners%E6%9F%A5%E8%AF%A2%E6%89%80%E6%9C%89%E7%9B%91%E5%90%AC\n\tdescribeAppAllListenersRequest := &bceappblb.DescribeAppListenerArgs{\n\t\tListenerPort: uint16(d.config.ListenerPort),\n\t}\n\tdescribeAppAllListenersResp, err := d.sdkClient.DescribeAppAllListeners(d.config.LoadbalancerId, describeAppAllListenersRequest)\n\td.logger.Debug(\"sdk request 'appblb.DescribeAppAllListeners'\", slog.String(\"blbId\", d.config.LoadbalancerId), slog.Any(\"request\", describeAppAllListenersRequest), slog.Any(\"response\", describeAppAllListenersResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'appblb.DescribeAppAllListeners': %w\", err)\n\t}\n\n\t// 获取全部 HTTPS/SSL 监听端口\n\tlisteners := make([]struct {\n\t\tType string\n\t\tPort int32\n\t}, 0)\n\tfor _, listener := range describeAppAllListenersResp.ListenerList {\n\t\tif listener.ListenerType == \"HTTPS\" || listener.ListenerType == \"SSL\" {\n\t\t\tlisteners = append(listeners, struct {\n\t\t\t\tType string\n\t\t\t\tPort int32\n\t\t\t}{\n\t\t\t\tType: listener.ListenerType,\n\t\t\t\tPort: int32(listener.ListenerPort),\n\t\t\t})\n\t\t}\n\t}\n\n\t// 遍历更新监听证书\n\tif len(listeners) == 0 {\n\t\td.logger.Info(\"no blb listeners to deploy\")\n\t} else {\n\t\td.logger.Info(\"found https/ssl listeners to deploy\", slog.Any(\"listeners\", listeners))\n\t\tvar errs []error\n\n\t\tfor _, listener := range listeners {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\n\t\t\tdefault:\n\t\t\t\tif err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudListenerType string, cloudListenerPort int32, cloudCertId string) error {\n\tswitch strings.ToUpper(cloudListenerType) {\n\tcase \"HTTPS\":\n\t\treturn d.updateHttpsListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId)\n\tcase \"SSL\":\n\t\treturn d.updateSslListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported listener type '%s'\", cloudListenerType)\n\t}\n}\n\nfunc (d *Deployer) updateHttpsListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error {\n\t// 查询 HTTPS 监听器\n\t// REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#describeapphttpslisteners%E6%9F%A5%E8%AF%A2https%E7%9B%91%E5%90%AC%E5%99%A8\n\tdescribeAppHTTPSListenersReq := &bceappblb.DescribeAppListenerArgs{\n\t\tListenerPort: uint16(cloudHttpsListenerPort),\n\t\tMaxKeys:      1,\n\t}\n\tdescribeAppHTTPSListenersResp, err := d.sdkClient.DescribeAppHTTPSListeners(cloudLoadbalancerId, describeAppHTTPSListenersReq)\n\td.logger.Debug(\"sdk request 'appblb.DescribeAppHTTPSListeners'\", slog.String(\"blbId\", cloudLoadbalancerId), slog.Any(\"request\", describeAppHTTPSListenersReq), slog.Any(\"response\", describeAppHTTPSListenersResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'appblb.DescribeAppHTTPSListeners': %w\", err)\n\t} else if len(describeAppHTTPSListenersResp.ListenerList) == 0 {\n\t\treturn fmt.Errorf(\"could not find listener '%s:%d'\", cloudLoadbalancerId, cloudHttpsListenerPort)\n\t}\n\n\tif d.config.Domain == \"\" {\n\t\t// 未指定 SNI，只需部署到监听器\n\n\t\t// 更新 HTTPS 监听器\n\t\t// REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#updateapphttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8\n\t\tupdateAppHTTPSListenerReq := &bceappblb.UpdateAppHTTPSListenerArgs{\n\t\t\tClientToken:  security.RandomString(32),\n\t\t\tListenerPort: uint16(cloudHttpsListenerPort),\n\t\t\tScheduler:    describeAppHTTPSListenersResp.ListenerList[0].Scheduler,\n\t\t\tCertIds:      []string{cloudCertId},\n\t\t}\n\t\terr := d.sdkClient.UpdateAppHTTPSListener(cloudLoadbalancerId, updateAppHTTPSListenerReq)\n\t\td.logger.Debug(\"sdk request 'appblb.UpdateAppHTTPSListener'\", slog.Any(\"request\", updateAppHTTPSListenerReq))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'appblb.UpdateAppHTTPSListener': %w\", err)\n\t\t}\n\t} else {\n\t\t// 指定 SNI，需部署到扩展域名\n\n\t\t// 更新 HTTPS 监听器\n\t\t// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8\n\t\tupdateAppHTTPSListenerReq := &bceappblb.UpdateAppHTTPSListenerArgs{\n\t\t\tClientToken:  security.RandomString(32),\n\t\t\tListenerPort: uint16(cloudHttpsListenerPort),\n\t\t\tScheduler:    describeAppHTTPSListenersResp.ListenerList[0].Scheduler,\n\t\t\tCertIds:      describeAppHTTPSListenersResp.ListenerList[0].CertIds,\n\t\t\tAdditionalCertDomains: lo.Map(describeAppHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceappblb.AdditionalCertDomainsModel, _ int) bceappblb.AdditionalCertDomainsModel {\n\t\t\t\tif domain.Host == d.config.Domain {\n\t\t\t\t\treturn bceappblb.AdditionalCertDomainsModel{\n\t\t\t\t\t\tHost:   domain.Host,\n\t\t\t\t\t\tCertId: cloudCertId,\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn bceappblb.AdditionalCertDomainsModel{\n\t\t\t\t\tHost:   domain.Host,\n\t\t\t\t\tCertId: domain.CertId,\n\t\t\t\t}\n\t\t\t}),\n\t\t}\n\t\terr := d.sdkClient.UpdateAppHTTPSListener(cloudLoadbalancerId, updateAppHTTPSListenerReq)\n\t\td.logger.Debug(\"sdk request 'appblb.UpdateAppHTTPSListener'\", slog.Any(\"request\", updateAppHTTPSListenerReq))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'appblb.UpdateAppHTTPSListener': %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateSslListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error {\n\t// 更新 SSL 监听器\n\t// REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#updateappssllistener%E6%9B%B4%E6%96%B0ssl%E7%9B%91%E5%90%AC%E5%99%A8\n\tupdateAppSSLListenerReq := &bceappblb.UpdateAppSSLListenerArgs{\n\t\tClientToken:  security.RandomString(32),\n\t\tListenerPort: uint16(cloudHttpsListenerPort),\n\t\tCertIds:      []string{cloudCertId},\n\t}\n\terr := d.sdkClient.UpdateAppSSLListener(cloudLoadbalancerId, updateAppSSLListenerReq)\n\td.logger.Debug(\"sdk request 'appblb.UpdateAppSSLListener'\", slog.Any(\"request\", updateAppSSLListenerReq))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'appblb.UpdateAppSSLListener': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey, region string) (*bceappblb.Client, error) {\n\tendpoint := \"\"\n\tif region != \"\" {\n\t\tendpoint = fmt.Sprintf(\"blb.%s.baidubce.com\", region)\n\t}\n\n\tclient, err := bceappblb.NewClient(accessKeyId, secretAccessKey, endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb_test.go",
    "content": "package baiducloudappblb_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baiducloud-appblb\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n\tfRegion          string\n\tfLoadbalancerId  string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"BAIDUCLOUDAPPBLB_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fLoadbalancerId, argsPrefix+\"LOADBALANCERID\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./baiducloud_appblb_test.go -args \\\n\t--BAIDUCLOUDAPPBLB_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--BAIDUCLOUDAPPBLB_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--BAIDUCLOUDAPPBLB_ACCESSKEYID=\"your-access-key-id\" \\\n\t--BAIDUCLOUDAPPBLB_SECRETACCESSKEY=\"your-secret-access-key\" \\\n\t--BAIDUCLOUDAPPBLB_REGION=\"bj\" \\\n\t--BAIDUCLOUDAPPBLB_LOADBALANCERID=\"your-blb-loadbalancer-id\" \\\n\t--BAIDUCLOUDAPPBLB_DOMAIN=\"your-blb-sni-domain\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LOADBALANCERID: %v\", fLoadbalancerId),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LOADBALANCER,\n\t\t\tRegion:          fRegion,\n\t\t\tLoadbalancerId:  fLoadbalancerId,\n\t\t\tDomain:          fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baiducloud-appblb/consts.go",
    "content": "package baiducloudappblb\n\nconst (\n\t// 资源类型：部署到指定负载均衡器。\n\tRESOURCE_TYPE_LOADBALANCER = \"loadbalancer\"\n\t// 资源类型：部署到指定监听器。\n\tRESOURCE_TYPE_LISTENER = \"listener\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go",
    "content": "package baiducloudblb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"strings\"\n\n\tbceblb \"github.com/baidubce/bce-sdk-go/services/blb\"\n\t\"github.com/pocketbase/pocketbase/tools/security\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/baiducloud-cert\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// 百度智能云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 百度智能云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 百度智能云区域。\n\tRegion string `json:\"region\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 负载均衡实例 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时必填。\n\tLoadbalancerId string `json:\"loadbalancerId,omitempty\"`\n\t// 负载均衡监听端口。\n\t// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。\n\tListenerPort int32 `json:\"listenerPort,omitempty\"`\n\t// SNI 域名（支持泛域名）。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。\n\tDomain string `json:\"domain,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *bceblb.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tSecretAccessKey: config.SecretAccessKey,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_LOADBALANCER:\n\t\tif err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_LISTENER:\n\t\tif err := d.deployToListener(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\n\t// 查询 BLB 实例详情\n\t// REF: https://cloud.baidu.com/doc/BLB/s/njwvxnv79#describeloadbalancerdetail%E6%9F%A5%E8%AF%A2blb%E5%AE%9E%E4%BE%8B%E8%AF%A6%E6%83%85\n\tdescribeLoadBalancerDetailResp, err := d.sdkClient.DescribeLoadBalancerDetail(d.config.LoadbalancerId)\n\td.logger.Debug(\"sdk request 'blb.DescribeLoadBalancerAttribute'\", slog.String(\"blbId\", d.config.LoadbalancerId), slog.Any(\"response\", describeLoadBalancerDetailResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'blb.DescribeLoadBalancerDetail': %w\", err)\n\t}\n\n\t// 获取全部 HTTPS/SSL 监听端口\n\tlisteners := make([]struct {\n\t\tType string\n\t\tPort int32\n\t}, 0)\n\tfor _, listener := range describeLoadBalancerDetailResp.Listener {\n\t\tif listener.Type == \"HTTPS\" || listener.Type == \"SSL\" {\n\t\t\tlistenerPort, err := strconv.Atoi(listener.Port)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlisteners = append(listeners, struct {\n\t\t\t\tType string\n\t\t\t\tPort int32\n\t\t\t}{\n\t\t\t\tType: listener.Type,\n\t\t\t\tPort: int32(listenerPort),\n\t\t\t})\n\t\t}\n\t}\n\n\t// 遍历更新监听证书\n\tif len(listeners) == 0 {\n\t\td.logger.Info(\"no blb listeners to deploy\")\n\t} else {\n\t\td.logger.Info(\"found https/ssl listeners to deploy\", slog.Any(\"listeners\", listeners))\n\t\tvar errs []error\n\n\t\tfor _, listener := range listeners {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\n\t\t\tdefault:\n\t\t\t\tif err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\tif d.config.ListenerPort == 0 {\n\t\treturn errors.New(\"config `listenerPort` is required\")\n\t}\n\n\t// 查询监听\n\t// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#describealllisteners%E6%9F%A5%E8%AF%A2%E6%89%80%E6%9C%89%E7%9B%91%E5%90%AC\n\tdescribeAllListenersRequest := &bceblb.DescribeListenerArgs{\n\t\tListenerPort: uint16(d.config.ListenerPort),\n\t}\n\tdescribeAllListenersResp, err := d.sdkClient.DescribeAllListeners(d.config.LoadbalancerId, describeAllListenersRequest)\n\td.logger.Debug(\"sdk request 'blb.DescribeAllListeners'\", slog.String(\"blbId\", d.config.LoadbalancerId), slog.Any(\"request\", describeAllListenersRequest), slog.Any(\"response\", describeAllListenersResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'blb.DescribeAllListeners': %w\", err)\n\t}\n\n\t// 获取全部 HTTPS/SSL 监听端口\n\tlisteners := make([]struct {\n\t\tType string\n\t\tPort int32\n\t}, 0)\n\tfor _, listener := range describeAllListenersResp.AllListenerList {\n\t\tif listener.ListenerType == \"HTTPS\" || listener.ListenerType == \"SSL\" {\n\t\t\tlisteners = append(listeners, struct {\n\t\t\t\tType string\n\t\t\t\tPort int32\n\t\t\t}{\n\t\t\t\tType: listener.ListenerType,\n\t\t\t\tPort: int32(listener.ListenerPort),\n\t\t\t})\n\t\t}\n\t}\n\n\t// 遍历更新监听证书\n\tif len(listeners) == 0 {\n\t\td.logger.Info(\"no blb listeners to deploy\")\n\t} else {\n\t\td.logger.Info(\"found https/ssl listeners to deploy\", slog.Any(\"listeners\", listeners))\n\t\tvar errs []error\n\n\t\tfor _, listener := range listeners {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\n\t\t\tdefault:\n\t\t\t\tif err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudListenerType string, cloudListenerPort int32, cloudCertId string) error {\n\tswitch strings.ToUpper(cloudListenerType) {\n\tcase \"HTTPS\":\n\t\treturn d.updateHttpsListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId)\n\tcase \"SSL\":\n\t\treturn d.updateSslListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported listener type '%s'\", cloudListenerType)\n\t}\n}\n\nfunc (d *Deployer) updateHttpsListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error {\n\t// 查询 HTTPS 监听器\n\t// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#describehttpslisteners%E6%9F%A5%E8%AF%A2https%E7%9B%91%E5%90%AC%E5%99%A8\n\tdescribeHTTPSListenersReq := &bceblb.DescribeListenerArgs{\n\t\tListenerPort: uint16(cloudHttpsListenerPort),\n\t\tMaxKeys:      1,\n\t}\n\tdescribeHTTPSListenersResp, err := d.sdkClient.DescribeHTTPSListeners(cloudLoadbalancerId, describeHTTPSListenersReq)\n\td.logger.Debug(\"sdk request 'blb.DescribeHTTPSListeners'\", slog.String(\"blbId\", cloudLoadbalancerId), slog.Any(\"request\", describeHTTPSListenersReq), slog.Any(\"response\", describeHTTPSListenersResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'blb.DescribeHTTPSListeners': %w\", err)\n\t} else if len(describeHTTPSListenersResp.ListenerList) == 0 {\n\t\treturn fmt.Errorf(\"could not find listener '%s:%d'\", cloudLoadbalancerId, cloudHttpsListenerPort)\n\t}\n\n\tif d.config.Domain == \"\" {\n\t\t// 未指定 SNI，只需部署到监听器\n\n\t\t// 更新 HTTPS 监听器\n\t\t// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8\n\t\tupdateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{\n\t\t\tClientToken:  security.RandomString(32),\n\t\t\tListenerPort: uint16(cloudHttpsListenerPort),\n\t\t\tCertIds:      []string{cloudCertId},\n\t\t}\n\t\terr := d.sdkClient.UpdateHTTPSListener(cloudLoadbalancerId, updateHTTPSListenerReq)\n\t\td.logger.Debug(\"sdk request 'blb.UpdateHTTPSListener'\", slog.Any(\"request\", updateHTTPSListenerReq))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'blb.UpdateHTTPSListener': %w\", err)\n\t\t}\n\t} else {\n\t\t// 指定 SNI，需部署到扩展域名\n\n\t\t// 更新 HTTPS 监听器\n\t\t// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8\n\t\tupdateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{\n\t\t\tClientToken:  security.RandomString(32),\n\t\t\tListenerPort: uint16(cloudHttpsListenerPort),\n\t\t\tCertIds:      describeHTTPSListenersResp.ListenerList[0].CertIds,\n\t\t\tAdditionalCertDomains: lo.Map(describeHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceblb.AdditionalCertDomainsModel, _ int) bceblb.AdditionalCertDomainsModel {\n\t\t\t\tif domain.Host == d.config.Domain {\n\t\t\t\t\treturn bceblb.AdditionalCertDomainsModel{\n\t\t\t\t\t\tHost:   domain.Host,\n\t\t\t\t\t\tCertId: cloudCertId,\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn bceblb.AdditionalCertDomainsModel{\n\t\t\t\t\tHost:   domain.Host,\n\t\t\t\t\tCertId: domain.CertId,\n\t\t\t\t}\n\t\t\t}),\n\t\t}\n\t\terr := d.sdkClient.UpdateHTTPSListener(cloudLoadbalancerId, updateHTTPSListenerReq)\n\t\td.logger.Debug(\"sdk request 'blb.UpdateHTTPSListener'\", slog.Any(\"request\", updateHTTPSListenerReq))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'blb.UpdateHTTPSListener': %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateSslListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error {\n\t// 更新 SSL 监听器\n\t// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatessllistener%E6%9B%B4%E6%96%B0ssl%E7%9B%91%E5%90%AC%E5%99%A8\n\tupdateSSLListenerReq := &bceblb.UpdateSSLListenerArgs{\n\t\tClientToken:  security.RandomString(32),\n\t\tListenerPort: uint16(cloudHttpsListenerPort),\n\t\tCertIds:      []string{cloudCertId},\n\t}\n\terr := d.sdkClient.UpdateSSLListener(cloudLoadbalancerId, updateSSLListenerReq)\n\td.logger.Debug(\"sdk request 'blb.UpdateSSLListener'\", slog.Any(\"request\", updateSSLListenerReq))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'blb.UpdateSSLListener': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey, region string) (*bceblb.Client, error) {\n\tendpoint := \"\"\n\tif region != \"\" {\n\t\tendpoint = fmt.Sprintf(\"blb.%s.baidubce.com\", region)\n\t}\n\n\tclient, err := bceblb.NewClient(accessKeyId, secretAccessKey, endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb_test.go",
    "content": "package baiducloudblb_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baiducloud-blb\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n\tfRegion          string\n\tfLoadbalancerId  string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"BAIDUCLOUDBLB_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fLoadbalancerId, argsPrefix+\"LOADBALANCERID\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./baiducloud_blb_test.go -args \\\n\t--BAIDUCLOUDBLB_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--BAIDUCLOUDBLB_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--BAIDUCLOUDBLB_ACCESSKEYID=\"your-access-key-id\" \\\n\t--BAIDUCLOUDBLB_SECRETACCESSKEY=\"your-secret-access-key\" \\\n\t--BAIDUCLOUDBLB_REGION=\"bj\" \\\n\t--BAIDUCLOUDBLB_LOADBALANCERID=\"your-blb-loadbalancer-id\" \\\n\t--BAIDUCLOUDBLB_DOMAIN=\"your-blb-sni-domain\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LOADBALANCERID: %v\", fLoadbalancerId),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LOADBALANCER,\n\t\t\tRegion:          fRegion,\n\t\t\tLoadbalancerId:  fLoadbalancerId,\n\t\t\tDomain:          fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baiducloud-blb/consts.go",
    "content": "package baiducloudblb\n\nconst (\n\t// 资源类型：部署到指定负载均衡器。\n\tRESOURCE_TYPE_LOADBALANCER = \"loadbalancer\"\n\t// 资源类型：部署到指定监听器。\n\tRESOURCE_TYPE_LISTENER = \"listener\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/baiducloud-cdn/baiducloud_cdn.go",
    "content": "package baiducloudcdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\tbcecdn \"github.com/baidubce/bce-sdk-go/services/cdn\"\n\tbcecdnapi \"github.com/baidubce/bce-sdk-go/services/cdn/api\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/samber/lo\"\n\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 百度智能云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 百度智能云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *bcecdn.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain)\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no cdn domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found cdn domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, certPEM, privkeyPEM); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// REF: https://cloud.baidu.com/doc/CDN/s/sjwvyewt1\n\tlistDomainsMarker := \"\"\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistDomainsRespDomains, listDomainsNextMarker, err := d.sdkClient.ListDomains(listDomainsMarker)\n\t\td.logger.Debug(\"sdk request 'cdn.ListDomains'\", slog.String(\"request.marker\", listDomainsMarker), slog.Any(\"response.domains\", listDomainsRespDomains))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.ListDomains': %w\", err)\n\t\t}\n\n\t\tdomains = append(domains, listDomainsRespDomains...)\n\n\t\tif listDomainsNextMarker == \"\" {\n\t\t\tbreak\n\t\t}\n\n\t\tlistDomainsMarker = listDomainsNextMarker\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, certPEM, privkeyPEM string) error {\n\t// 修改域名证书\n\t// REF: https://cloud.baidu.com/doc/CDN/s/qjzuz2hp8\n\tputCertResp, err := d.sdkClient.PutCert(\n\t\tdomain,\n\t\t&bcecdnapi.UserCertificate{\n\t\t\tCertName:    fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli()),\n\t\t\tServerData:  certPEM,\n\t\t\tPrivateData: privkeyPEM,\n\t\t},\n\t\t\"ON\",\n\t)\n\td.logger.Debug(\"sdk request 'cdn.PutCert'\", slog.String(\"request.domain\", domain), slog.Any(\"response\", putCertResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.PutCert': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey string) (*bcecdn.Client, error) {\n\tclient, err := bcecdn.NewClient(accessKeyId, secretAccessKey, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baiducloud-cdn/baiducloud_cdn_test.go",
    "content": "package baiducloudcdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baiducloud-cdn\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"BAIDUCLOUDCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./baiducloud_cdn_test.go -args \\\n\t--BAIDUCLOUDCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--BAIDUCLOUDCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--BAIDUCLOUDCDN_ACCESSKEYID=\"your-access-key-id\" \\\n\t--BAIDUCLOUDCDN_SECRETACCESSKEY=\"your-secret-access-key\" \\\n\t--BAIDUCLOUDCDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tSecretAccessKey:    fSecretAccessKey,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baiducloud-cdn/consts.go",
    "content": "package baiducloudcdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/baiducloud-cert/baiducloud_cert.go",
    "content": "package baiducloudcert\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/baiducloud-cert\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// 百度智能云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 百度智能云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tSecretAccessKey: config.SecretAccessKey,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baishan-cdn/baishan_cdn.go",
    "content": "package baishancdn\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/baishan-cdn\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbaishansdk \"github.com/certimate-go/certimate/pkg/sdk3rd/baishan\"\n)\n\ntype DeployerConfig struct {\n\t// 白山云 API Token。\n\tApiToken string `json:\"apiToken\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 域名匹配模式。暂时只支持精确匹配。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n\t// 证书 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。\n\tCertificateId string `json:\"certificateId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *baishansdk.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ApiToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tApiToken: config.ApiToken,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_DOMAIN:\n\t\tif err := d.deployToDomain(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_CERTIFICATE:\n\t\tif err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToDomain(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.Domain == \"\" {\n\t\treturn errors.New(\"config `domain` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 查询域名配置\n\t// REF: https://portal.baishancloud.com/track/document/api/1/1065\n\tgetDomainConfigReq := &baishansdk.GetDomainConfigRequest{\n\t\tDomains: lo.ToPtr(d.config.Domain),\n\t\tConfig:  lo.ToPtr([]string{\"https\"}),\n\t}\n\tgetDomainConfigResp, err := d.sdkClient.GetDomainConfigWithContext(ctx, getDomainConfigReq)\n\td.logger.Debug(\"sdk request 'baishan.GetDomainConfig'\", slog.Any(\"request\", getDomainConfigReq), slog.Any(\"response\", getDomainConfigResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'baishan.GetDomainConfig': %w\", err)\n\t} else if len(getDomainConfigResp.Data) == 0 {\n\t\treturn fmt.Errorf(\"could not find domain '%s'\", d.config.Domain)\n\t}\n\n\t// 设置域名配置\n\t// REF: https://portal.baishancloud.com/track/document/api/1/1045\n\tsetDomainConfigReq := &baishansdk.SetDomainConfigRequest{\n\t\tDomains: lo.ToPtr(d.config.Domain),\n\t\tConfig: &baishansdk.DomainConfig{\n\t\t\tHttps: &baishansdk.DomainConfigHttps{\n\t\t\t\tCertId:      json.Number(upres.CertId),\n\t\t\t\tForceHttps:  getDomainConfigResp.Data[0].Config.Https.ForceHttps,\n\t\t\t\tEnableHttp2: getDomainConfigResp.Data[0].Config.Https.EnableHttp2,\n\t\t\t\tEnableOcsp:  getDomainConfigResp.Data[0].Config.Https.EnableOcsp,\n\t\t\t},\n\t\t},\n\t}\n\tsetDomainConfigResp, err := d.sdkClient.SetDomainConfigWithContext(ctx, setDomainConfigReq)\n\td.logger.Debug(\"sdk request 'baishan.SetDomainConfig'\", slog.Any(\"request\", setDomainConfigReq), slog.Any(\"response\", setDomainConfigResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'baishan.SetDomainConfig': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.CertificateId == \"\" {\n\t\treturn errors.New(\"config `certificateId` is required\")\n\t}\n\n\t// 替换证书\n\topres, err := d.sdkCertmgr.Replace(ctx, d.config.CertificateId, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to replace certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate replaced\", slog.Any(\"result\", opres))\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(apiToken string) (*baishansdk.Client, error) {\n\treturn baishansdk.NewClient(apiToken)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baishan-cdn/baishan_cdn_test.go",
    "content": "package baishancdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baishan-cdn\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfApiToken      string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"BAISHANCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fApiToken, argsPrefix+\"APITOKEN\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./baishan_cdn_test.go -args \\\n\t--BAISHANCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--BAISHANCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--BAISHANCDN_APITOKEN=\"your-api-token\" \\\n\t--BAISHANCDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"APITOKEN: %v\", fApiToken),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tApiToken:           fApiToken,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baishan-cdn/consts.go",
    "content": "package baishancdn\n\nconst (\n\t// 资源类型：替换指定域名的证书。\n\tRESOURCE_TYPE_DOMAIN = \"domain\"\n\t// 资源类型：替换指定证书。\n\tRESOURCE_TYPE_CERTIFICATE = \"certificate\"\n)\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/baotapanel/baotapanel.go",
    "content": "package baotapanel\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbtsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/btpanel\"\n\txwait \"github.com/certimate-go/certimate/pkg/utils/wait\"\n)\n\ntype DeployerConfig struct {\n\t// 宝塔面板服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// 宝塔面板接口密钥。\n\tApiKey string `json:\"apiKey\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 网站类型。\n\tSiteType string `json:\"siteType\"`\n\t// 网站名称。\n\tSiteNames []string `json:\"siteNames,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *btsdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nvar btProjectTypes = []string{\"php\", \"java\", \"nodejs\", \"go\", \"python\", \"proxy\", \"html\", \"general\"}\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif len(d.config.SiteNames) == 0 {\n\t\treturn nil, errors.New(\"config `siteNames` is required\")\n\t}\n\n\tswitch d.config.SiteType {\n\tcase \"any\":\n\t\t{\n\t\t\t// 上传证书\n\t\t\tsslCertSaveCertReq := &btsdk.SSLCertSaveCertRequest{\n\t\t\t\tCertificate: certPEM,\n\t\t\t\tPrivateKey:  privkeyPEM,\n\t\t\t}\n\t\t\tsslCertSaveCertResp, err := d.sdkClient.SSLCertSaveCertWithContext(ctx, sslCertSaveCertReq)\n\t\t\td.logger.Debug(\"sdk request 'bt.SSLCertSaveCert'\", slog.Any(\"request\", sslCertSaveCertReq), slog.Any(\"response\", sslCertSaveCertResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'bt.SSLCertSaveCert': %w\", err)\n\t\t\t}\n\n\t\t\t// 设置站点证书\n\t\t\tsslSetBatchCertToSiteReq := &btsdk.SSLSetBatchCertToSiteRequest{\n\t\t\t\tBatchInfo: lo.Map(d.config.SiteNames, func(siteName string, _ int) *btsdk.SSLSetBatchCertToSiteRequestBatchInfo {\n\t\t\t\t\treturn &btsdk.SSLSetBatchCertToSiteRequestBatchInfo{\n\t\t\t\t\t\tSiteName: siteName,\n\t\t\t\t\t\tSSLHash:  sslCertSaveCertResp.SSLHash,\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\t}\n\t\t\tsslSetBatchCertToSiteResp, err := d.sdkClient.SSLSetBatchCertToSiteWithContext(ctx, sslSetBatchCertToSiteReq)\n\t\t\td.logger.Debug(\"sdk request 'bt.SSLSetBatchCertToSite'\", slog.Any(\"request\", sslSetBatchCertToSiteReq), slog.Any(\"response\", sslSetBatchCertToSiteResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'bt.SSLSetBatchCertToSite': %w\", err)\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\t{\n\t\t\tif d.config.SiteType != \"\" {\n\t\t\t\tif !lo.Contains(btProjectTypes, d.config.SiteType) {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unsupported site type: '%s'\", d.config.SiteType)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 遍历更新站点证书\n\t\t\tvar errs []error\n\t\t\tfor i, siteName := range d.config.SiteNames {\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn nil, ctx.Err()\n\t\t\t\tdefault:\n\t\t\t\t\tif err := d.updateSiteCertificate(ctx, d.config.SiteType, siteName, certPEM, privkeyPEM); err != nil {\n\t\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t\t}\n\t\t\t\t\tif i < len(d.config.SiteNames)-1 {\n\t\t\t\t\t\txwait.DelayWithContext(ctx, time.Second*5)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(errs) > 0 {\n\t\t\t\treturn nil, errors.Join(errs...)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) updateSiteCertificate(ctx context.Context, siteType, siteName string, certPEM, privkeyPEM string) error {\n\tswitch siteType {\n\tcase \"proxy\":\n\t\t{\n\t\t\t// 设置代理 SSL 证书\n\t\t\tmodProxyComSetSSLReq := &btsdk.ModProxyComSetSSLRequest{\n\t\t\t\tSiteName:    siteName,\n\t\t\t\tCertificate: certPEM,\n\t\t\t\tPrivateKey:  privkeyPEM,\n\t\t\t}\n\t\t\tmodProxyComSetSSLResp, err := d.sdkClient.ModProxyComSetSSLWithContext(ctx, modProxyComSetSSLReq)\n\t\t\td.logger.Debug(\"sdk request 'bt.ModProxyComSetSSL'\", slog.Any(\"request\", modProxyComSetSSLReq), slog.Any(\"response\", modProxyComSetSSLResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'bt.ModProxyComSetSSL': %w\", err)\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\t{\n\t\t\t// 设置站点 SSL 证书\n\t\t\tsiteSetSSLReq := &btsdk.SiteSetSSLRequest{\n\t\t\t\tType:        \"0\",\n\t\t\t\tSiteName:    siteName,\n\t\t\t\tCertificate: certPEM,\n\t\t\t\tPrivateKey:  privkeyPEM,\n\t\t\t}\n\t\t\tsiteSetSSLResp, err := d.sdkClient.SiteSetSSLWithContext(ctx, siteSetSSLReq)\n\t\t\td.logger.Debug(\"sdk request 'bt.SiteSetSSL'\", slog.Any(\"request\", siteSetSSLReq), slog.Any(\"response\", siteSetSSLResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'bt.SiteSetSSL': %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btsdk.Client, error) {\n\tclient, err := btsdk.NewClient(serverUrl, apiKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baotapanel/baotapanel_test.go",
    "content": "package baotapanel_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baotapanel\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfApiKey        string\n\tfSiteType      string\n\tfSiteName      string\n)\n\nfunc init() {\n\targsPrefix := \"BAOTAPANEL_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n\tflag.StringVar(&fSiteType, argsPrefix+\"SITETYPE\", \"\", \"\")\n\tflag.StringVar(&fSiteName, argsPrefix+\"SITENAME\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./baotapanel_test.go -args \\\n\t--BAOTAPANEL_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--BAOTAPANEL_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--BAOTAPANEL_SERVERURL=\"http://127.0.0.1:8888\" \\\n\t--BAOTAPANEL_APIKEY=\"your-api-key\" \\\n\t--BAOTAPANEL_SITETYPE=\"php\" \\\n\t--BAOTAPANEL_SITENAME=\"your-site-name\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t\tfmt.Sprintf(\"SITETYPE: %v\", fSiteType),\n\t\t\tfmt.Sprintf(\"SITENAME: %v\", fSiteName),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiKey:                   fApiKey,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tSiteType:                 fSiteType,\n\t\t\tSiteNames:                []string{fSiteName},\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baotapanel-console/baotapanel_console.go",
    "content": "package baotapanelconsole\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbtsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/btpanel\"\n)\n\ntype DeployerConfig struct {\n\t// 宝塔面板服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// 宝塔面板接口密钥。\n\tApiKey string `json:\"apiKey\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 是否自动重启。\n\tAutoRestart bool `json:\"autoRestart\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *btsdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 设置面板 SSL 证书\n\tconfigSavePanelSSLReq := &btsdk.ConfigSavePanelSSLRequest{\n\t\tPrivateKey:  privkeyPEM,\n\t\tCertificate: certPEM,\n\t}\n\tconfigSavePanelSSLResp, err := d.sdkClient.ConfigSavePanelSSLWithContext(ctx, configSavePanelSSLReq)\n\td.logger.Debug(\"sdk request 'bt.ConfigSavePanelSSL'\", slog.Any(\"request\", configSavePanelSSLReq), slog.Any(\"response\", configSavePanelSSLResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'bt.ConfigSavePanelSSL': %w\", err)\n\t}\n\n\tif d.config.AutoRestart {\n\t\t// 重启面板（无需关心响应，因为宝塔重启时会断开连接产生 error）\n\t\tsystemServiceAdminReq := &btsdk.SystemServiceAdminRequest{\n\t\t\tName: \"nginx\",\n\t\t\tType: \"restart\",\n\t\t}\n\t\tsystemServiceAdminResp, _ := d.sdkClient.SystemServiceAdminWithContext(ctx, systemServiceAdminReq)\n\t\td.logger.Debug(\"sdk request 'bt.SystemServiceAdmin'\", slog.Any(\"request\", systemServiceAdminReq), slog.Any(\"response\", systemServiceAdminResp))\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btsdk.Client, error) {\n\tclient, err := btsdk.NewClient(serverUrl, apiKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baotapanel-console/baotapanel_console_test.go",
    "content": "package baotapanelconsole_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baotapanel-console\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfApiKey        string\n)\n\nfunc init() {\n\targsPrefix := \"BAOTAPANELCONSOLE_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./baotapanel_console_test.go -args \\\n\t--BAOTAPANELCONSOLE_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--BAOTAPANELCONSOLE_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--BAOTAPANELCONSOLE_SERVERURL=\"http://127.0.0.1:8888\" \\\n\t--BAOTAPANELCONSOLE_APIKEY=\"your-api-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiKey:                   fApiKey,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tAutoRestart:              true,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baotapanelgo/baotapanelgo.go",
    "content": "package baotapanelgo\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbtsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/btpanelgo\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txwait \"github.com/certimate-go/certimate/pkg/utils/wait\"\n)\n\ntype DeployerConfig struct {\n\t// 宝塔面板服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// 宝塔面板接口密钥。\n\tApiKey string `json:\"apiKey\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 网站类型。\n\tSiteType string `json:\"siteType\"`\n\t// 网站名称。\n\tSiteNames []string `json:\"siteNames,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *btsdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nvar (\n\tbtProjectTypes      = []string{\"php\", \"java\", \"asp\", \"go\", \"python\", \"nodejs\", \"proxy\", \"general\"}\n\tbtProjectTypesInIIS = []string{\"php\", \"asp\", \"aspx\"}\n)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif len(d.config.SiteNames) == 0 {\n\t\treturn nil, errors.New(\"config `siteNames` is required\")\n\t}\n\n\tif d.config.SiteType != \"\" {\n\t\tif !lo.Contains(btProjectTypes, d.config.SiteType) && !lo.Contains(btProjectTypesInIIS, d.config.SiteType) {\n\t\t\treturn nil, fmt.Errorf(\"unsupported site type: '%s'\", d.config.SiteType)\n\t\t}\n\t}\n\n\t// 遍历更新站点证书\n\tvar errs []error\n\tfor i, siteName := range d.config.SiteNames {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t\tif err := d.updateSiteCertificate(ctx, d.config.SiteType, siteName, certPEM, privkeyPEM); err != nil {\n\t\t\t\terrs = append(errs, err)\n\t\t\t}\n\t\t\tif i < len(d.config.SiteNames)-1 {\n\t\t\t\txwait.DelayWithContext(ctx, time.Second*5)\n\t\t\t}\n\t\t}\n\t}\n\tif len(errs) > 0 {\n\t\treturn nil, errors.Join(errs...)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) findSiteByName(ctx context.Context, siteType, siteName string) (*btsdk.SiteData, error) {\n\tif siteType == \"\" || lo.Contains(btProjectTypesInIIS, siteType) {\n\t\t// 查询网站列表\n\t\tdatalistGetDataListPage := 1\n\t\tdatalistGetDataListLimit := 10\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\tdatalistGetDataListReq := &btsdk.DatalistGetDataListRequest{\n\t\t\t\tTable:        lo.ToPtr(\"sites\"),\n\t\t\t\tSearchString: lo.ToPtr(siteName),\n\t\t\t\tPage:         lo.ToPtr(int32(datalistGetDataListPage)),\n\t\t\t\tLimit:        lo.ToPtr(int32(datalistGetDataListLimit)),\n\t\t\t}\n\t\t\tdatalistGetDataListResp, err := d.sdkClient.DatalistGetDataListWithContext(ctx, datalistGetDataListReq)\n\t\t\td.logger.Debug(\"sdk request 'bt.DatalistGetDataList'\", slog.Any(\"request\", datalistGetDataListReq), slog.Any(\"response\", datalistGetDataListResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'bt.DatalistGetDataList': %w\", err)\n\t\t\t}\n\n\t\t\tfor _, siteItem := range datalistGetDataListResp.Data {\n\t\t\t\tif strings.EqualFold(siteItem.Name, siteName) {\n\t\t\t\t\treturn siteItem, nil\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(datalistGetDataListResp.Data) < datalistGetDataListLimit {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tdatalistGetDataListPage++\n\t\t}\n\t} else {\n\t\t// 查询网站列表\n\t\tsiteGetProjectListPage := 1\n\t\tsiteGetProjectListLimit := 10\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\tsiteGetProjectListReq := &btsdk.SiteGetProjectListRequest{\n\t\t\t\tSearchType:   lo.ToPtr(siteType),\n\t\t\t\tSearchString: lo.ToPtr(siteName),\n\t\t\t\tPage:         lo.ToPtr(int32(siteGetProjectListPage)),\n\t\t\t\tLimit:        lo.ToPtr(int32(siteGetProjectListLimit)),\n\t\t\t}\n\t\t\tsiteGetProjectListResp, err := d.sdkClient.SiteGetProjectListWithContext(ctx, siteGetProjectListReq)\n\t\t\td.logger.Debug(\"sdk request 'bt.SiteGetProjectList'\", slog.Any(\"request\", siteGetProjectListReq), slog.Any(\"response\", siteGetProjectListResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'bt.SiteGetProjectList': %w\", err)\n\t\t\t}\n\n\t\t\tfor _, siteItem := range siteGetProjectListResp.Data {\n\t\t\t\tif strings.EqualFold(siteItem.Name, siteName) {\n\t\t\t\t\treturn siteItem, nil\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(siteGetProjectListResp.Data) < siteGetProjectListLimit {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tsiteGetProjectListPage++\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"could not find site '%s'\", siteName)\n}\n\nfunc (d *Deployer) updateSiteCertificate(ctx context.Context, siteType, siteName string, certPEM, privkeyPEM string) error {\n\t// 获取面板配置\n\tpanelGetConfigReq := &btsdk.PanelGetConfigRequest{}\n\tpanelGetConfigResp, err := d.sdkClient.PanelGetConfigWithContext(ctx, panelGetConfigReq)\n\td.logger.Debug(\"sdk request 'bt.PanelGetConfig'\", slog.Any(\"request\", panelGetConfigReq), slog.Any(\"response\", panelGetConfigResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'bt.PanelGetConfig': %w\", err)\n\t}\n\n\t// 获取网站\n\tsiteData, err := d.findSiteByName(ctx, siteType, siteName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 根据服务器类型部署证书\n\tpfxRequried := panelGetConfigResp.Site != nil && strings.EqualFold(panelGetConfigResp.Site.WebServer, \"iis\")\n\tif pfxRequried {\n\t\t// 转换证书格式\n\t\tcertPFXPassword := \"certimate\"\n\t\tcertPFX, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, certPFXPassword)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to transform certificate from PEM to PFX: %w\", err)\n\t\t}\n\n\t\t// 上传证书\n\t\tcertPFXHash := sha256.Sum256([]byte(certPFX))\n\t\tcertPFXHashHex := hex.EncodeToString(certPFXHash[:])\n\t\tcertPFXPath := panelGetConfigResp.Paths.Soft + \"/temp/ssl/certimate\"\n\t\tcertPFXFileName := fmt.Sprintf(\"%s.pfx\", certPFXHashHex)\n\t\tfilesUploadReq := &btsdk.FilesUploadRequest{\n\t\t\tPath:  lo.ToPtr(certPFXPath),\n\t\t\tName:  lo.ToPtr(certPFXFileName),\n\t\t\tStart: lo.ToPtr(int32(0)),\n\t\t\tSize:  lo.ToPtr(int32(len(certPFX))),\n\t\t\tBlob:  certPFX,\n\t\t\tForce: lo.ToPtr(true),\n\t\t}\n\t\tfilesUploadResp, err := d.sdkClient.FilesUploadWithContext(ctx, filesUploadReq)\n\t\td.logger.Debug(\"sdk request 'bt.FilesUpload'\", slog.Any(\"request\", filesUploadReq), slog.Any(\"response\", filesUploadResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'bt.FilesUpload': %w\", err)\n\t\t}\n\n\t\t// 服务器为 IIS，设置网站 SSL\n\t\tsiteSetSitePFXSSLReq := &btsdk.SiteSetSitePFXSSLRequest{\n\t\t\tSiteId:   lo.ToPtr(siteData.Id),\n\t\t\tPFX:      lo.ToPtr(fmt.Sprintf(\"%s/%s\", certPFXPath, certPFXFileName)),\n\t\t\tPassword: lo.ToPtr(certPFXPassword),\n\t\t}\n\t\tsiteSetSitePFXSSLResp, err := d.sdkClient.SiteSetSitePFXSSLWithContext(ctx, siteSetSitePFXSSLReq)\n\t\td.logger.Debug(\"sdk request 'bt.SiteSetSitePFXSSL'\", slog.Any(\"request\", siteSetSitePFXSSLReq), slog.Any(\"response\", siteSetSitePFXSSLResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'bt.SiteSetSitePFXSSL': %w\", err)\n\t\t}\n\t} else {\n\t\t// 服务器非 IIS，设置网站 SSL\n\t\tsiteSetSiteSSLReq := &btsdk.SiteSetSiteSSLRequest{\n\t\t\tSiteId: lo.ToPtr(siteData.Id),\n\t\t\tStatus: lo.ToPtr(true),\n\t\t\tKey:    lo.ToPtr(privkeyPEM),\n\t\t\tCert:   lo.ToPtr(certPEM),\n\t\t}\n\t\tsiteSetSiteSSLResp, err := d.sdkClient.SiteSetSiteSSLWithContext(ctx, siteSetSiteSSLReq)\n\t\td.logger.Debug(\"sdk request 'bt.SiteSetSiteSSL'\", slog.Any(\"request\", siteSetSiteSSLReq), slog.Any(\"response\", siteSetSiteSSLResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'bt.SiteSetSiteSSL': %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btsdk.Client, error) {\n\tclient, err := btsdk.NewClient(serverUrl, apiKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baotapanelgo/baotapanelgo_test.go",
    "content": "package baotapanelgo_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baotapanelgo\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfApiKey        string\n\tfSiteType      string\n\tfSiteName      string\n)\n\nfunc init() {\n\targsPrefix := \"BAOTAPANELGO_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n\tflag.StringVar(&fSiteType, argsPrefix+\"SITETYPE\", \"\", \"\")\n\tflag.StringVar(&fSiteName, argsPrefix+\"SITENAME\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./baotapanelgo_test.go -args \\\n\t--BAOTAPANELGO_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--BAOTAPANELGO_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--BAOTAPANELGO_SERVERURL=\"http://127.0.0.1:8888\" \\\n\t--BAOTAPANELGO_APIKEY=\"your-api-key\" \\\n\t--BAOTAPANELGO_SITETYPE=\"your-site-type\" \\\n\t--BAOTAPANELGO_SITENAME=\"your-site-name\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t\tfmt.Sprintf(\"SITETYPE: %v\", fSiteType),\n\t\t\tfmt.Sprintf(\"SITENAME: %v\", fSiteName),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiKey:                   fApiKey,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tSiteType:                 fSiteType,\n\t\t\tSiteNames:                []string{fSiteName},\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baotapanelgo-console/baotapanelgo_console.go",
    "content": "package baotapanelgoconsole\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbtsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/btpanelgo\"\n)\n\ntype DeployerConfig struct {\n\t// 宝塔面板服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// 宝塔面板接口密钥。\n\tApiKey string `json:\"apiKey\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *btsdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 设置面板 SSL 证书\n\tconfigSetPanelSSLReq := &btsdk.ConfigSetPanelSSLRequest{\n\t\tSSLStatus: lo.ToPtr(int32(1)),\n\t\tSSLKey:    lo.ToPtr(privkeyPEM),\n\t\tSSLPem:    lo.ToPtr(certPEM),\n\t}\n\tconfigSetPanelSSLResp, err := d.sdkClient.ConfigSetPanelSSLWithContext(ctx, configSetPanelSSLReq)\n\td.logger.Debug(\"sdk request 'bt.ConfigSetPanelSSL'\", slog.Any(\"request\", configSetPanelSSLReq), slog.Any(\"response\", configSetPanelSSLResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'bt.ConfigSetPanelSSL': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btsdk.Client, error) {\n\tclient, err := btsdk.NewClient(serverUrl, apiKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baotapanelgo-console/baotapanelgo_console_test.go",
    "content": "package baotapanelgoconsole_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baotapanelgo-console\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfApiKey        string\n)\n\nfunc init() {\n\targsPrefix := \"BAOTAPANELGOCONSOLE_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./baotapanelgo_console_test.go -args \\\n\t--BAOTAPANELGOCONSOLE_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--BAOTAPANELGOCONSOLE_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--BAOTAPANELGOCONSOLE_SERVERURL=\"http://127.0.0.1:8888\" \\\n\t--BAOTAPANELGOCONSOLE_APIKEY=\"your-api-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiKey:                   fApiKey,\n\t\t\tAllowInsecureConnections: true,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baotawaf/baotawaf.go",
    "content": "package baotawaf\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbtwafsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/btwaf\"\n\txwait \"github.com/certimate-go/certimate/pkg/utils/wait\"\n)\n\ntype DeployerConfig struct {\n\t// 堡塔云 WAF 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// 堡塔云 WAF 接口密钥。\n\tApiKey string `json:\"apiKey\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 网站名称。\n\tSiteNames []string `json:\"siteNames\"`\n\t// 网站 SSL 端口。\n\t// 零值时默认值 443。\n\tSitePort int32 `json:\"sitePort,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *btwafsdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif len(d.config.SiteNames) == 0 {\n\t\treturn nil, errors.New(\"config `siteNames` is required\")\n\t}\n\n\t// 遍历更新站点证书\n\tvar errs []error\n\tfor i, siteName := range d.config.SiteNames {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t\tif err := d.updateSiteCertificate(ctx, siteName, d.config.SitePort, certPEM, privkeyPEM); err != nil {\n\t\t\t\terrs = append(errs, err)\n\t\t\t}\n\t\t\tif i < len(d.config.SiteNames)-1 {\n\t\t\t\txwait.DelayWithContext(ctx, time.Second*5)\n\t\t\t}\n\t\t}\n\t}\n\tif len(errs) > 0 {\n\t\treturn nil, errors.Join(errs...)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) findSiteByName(ctx context.Context, siteName string) (*btwafsdk.SiteRecord, error) {\n\t// 查询网站列表\n\tgetSiteListPage := 1\n\tgetSiteListPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tgetSiteListReq := &btwafsdk.GetSiteListRequest{\n\t\t\tSiteName: lo.ToPtr(siteName),\n\t\t\tPage:     lo.ToPtr(int32(getSiteListPage)),\n\t\t\tPageSize: lo.ToPtr(int32(getSiteListPageSize)),\n\t\t}\n\t\tgetSiteListResp, err := d.sdkClient.GetSiteListWithContext(ctx, getSiteListReq)\n\t\td.logger.Debug(\"sdk request 'bt.GetSiteList'\", slog.Any(\"request\", getSiteListReq), slog.Any(\"response\", getSiteListResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'bt.GetSiteList': %w\", err)\n\t\t}\n\n\t\tif getSiteListResp.Result == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, siteItem := range getSiteListResp.Result.List {\n\t\t\tif siteItem.SiteName == siteName {\n\t\t\t\treturn siteItem, nil\n\t\t\t}\n\t\t}\n\n\t\tif len(getSiteListResp.Result.List) < getSiteListPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tgetSiteListPage++\n\t}\n\n\treturn nil, fmt.Errorf(\"could not find site '%s'\", siteName)\n}\n\nfunc (d *Deployer) updateSiteCertificate(ctx context.Context, siteName string, sitePort int32, certPEM, privkeyPEM string) error {\n\tif sitePort == 0 {\n\t\tsitePort = 443\n\t}\n\n\t// 获取网站配置\n\tsiteData, err := d.findSiteByName(ctx, siteName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 修改网站配置\n\tmodifySiteReq := &btwafsdk.ModifySiteRequest{\n\t\tSiteId: lo.ToPtr(siteData.SiteId),\n\t\tType:   lo.ToPtr(\"openCert\"),\n\t\tServer: &btwafsdk.SiteServerInfoMod{\n\t\t\tListenSSLPorts: lo.ToPtr([]string{fmt.Sprintf(\"%d\", d.config.SitePort)}),\n\t\t\tSSL: &btwafsdk.SiteServerSSLInfo{\n\t\t\t\tIsSSL:      lo.ToPtr(int32(1)),\n\t\t\t\tFullChain:  lo.ToPtr(certPEM),\n\t\t\t\tPrivateKey: lo.ToPtr(privkeyPEM),\n\t\t\t},\n\t\t},\n\t}\n\tmodifySiteResp, err := d.sdkClient.ModifySiteWithContext(ctx, modifySiteReq)\n\td.logger.Debug(\"sdk request 'bt.ModifySite'\", slog.Any(\"request\", modifySiteReq), slog.Any(\"response\", modifySiteResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'bt.ModifySite': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btwafsdk.Client, error) {\n\tclient, err := btwafsdk.NewClient(serverUrl, apiKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baotawaf/baotawaf_test.go",
    "content": "package baotawaf_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baotawaf\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfApiKey        string\n\tfSiteName      string\n\tfSitePort      int64\n)\n\nfunc init() {\n\targsPrefix := \"BAOTAWAF_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n\tflag.StringVar(&fSiteName, argsPrefix+\"SITENAME\", \"\", \"\")\n\tflag.Int64Var(&fSitePort, argsPrefix+\"SITEPORT\", 0, \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./baotawaf_test.go -args \\\n\t--BAOTAWAF_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--BAOTAWAF_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--BAOTAWAF_SERVERURL=\"http://127.0.0.1:8888\" \\\n\t--BAOTAWAF_APIKEY=\"your-api-key\" \\\n\t--BAOTAWAF_SITENAME=\"your-site-name\" \\\n\t--BAOTAWAF_SITEPORT=443\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t\tfmt.Sprintf(\"SITENAME: %v\", fSiteName),\n\t\t\tfmt.Sprintf(\"SITEPORT: %v\", fSitePort),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiKey:                   fApiKey,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tSiteNames:                []string{fSiteName},\n\t\t\tSitePort:                 int32(fSitePort),\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baotawaf-console/baotawaf_console.go",
    "content": "package baotapanelconsole\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbtwafsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/btwaf\"\n)\n\ntype DeployerConfig struct {\n\t// 堡塔云 WAF 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// 堡塔云 WAF 接口密钥。\n\tApiKey string `json:\"apiKey\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *btwafsdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 设置面板 SSL\n\tconfigSetCertReq := &btwafsdk.ConfigSetCertRequest{\n\t\tCertContent: lo.ToPtr(certPEM),\n\t\tKeyContent:  lo.ToPtr(privkeyPEM),\n\t}\n\tconfigSetCertResp, err := d.sdkClient.ConfigSetCertWithContext(ctx, configSetCertReq)\n\td.logger.Debug(\"sdk request 'bt.ConfigSetCert'\", slog.Any(\"request\", configSetCertReq), slog.Any(\"response\", configSetCertResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'bt.ConfigSetCert': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btwafsdk.Client, error) {\n\tclient, err := btwafsdk.NewClient(serverUrl, apiKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/baotawaf-console/baotawaf_console_test.go",
    "content": "package baotapanelconsole_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/baotawaf-console\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfApiKey        string\n\tfSiteName      string\n\tfSitePort      int64\n)\n\nfunc init() {\n\targsPrefix := \"BAOTAWAFCONSOLE_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./baotawaf_console_test.go -args \\\n\t--BAOTAWAFCONSOLE_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--BAOTAWAFCONSOLE_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--BAOTAWAFCONSOLE_SERVERURL=\"http://127.0.0.1:8888\" \\\n\t--BAOTAWAFCONSOLE_APIKEY=\"your-api-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiKey:                   fApiKey,\n\t\t\tAllowInsecureConnections: true,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/bunny-cdn/bunny_cdn.go",
    "content": "package bunnycdn\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tbunnysdk \"github.com/certimate-go/certimate/pkg/sdk3rd/bunny\"\n)\n\ntype DeployerConfig struct {\n\t// Bunny API Key。\n\tApiKey string `json:\"apiKey\"`\n\t// Bunny Pull Zone ID。\n\tPullZoneId string `json:\"pullZoneId\"`\n\t// Bunny CDN Hostname。\n\tHostname string `json:\"hostname\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *bunnysdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ApiKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.PullZoneId == \"\" {\n\t\treturn nil, fmt.Errorf(\"config `pullZoneId` is required\")\n\t}\n\tif d.config.Hostname == \"\" {\n\t\treturn nil, fmt.Errorf(\"config `hostname` is required\")\n\t}\n\n\t// 上传证书\n\tcreateCertificateReq := &bunnysdk.AddCustomCertificateRequest{\n\t\tHostname:       d.config.Hostname,\n\t\tCertificate:    base64.StdEncoding.EncodeToString([]byte(certPEM)),\n\t\tCertificateKey: base64.StdEncoding.EncodeToString([]byte(privkeyPEM)),\n\t}\n\terr := d.sdkClient.AddCustomCertificateWithContext(ctx, d.config.PullZoneId, createCertificateReq)\n\td.logger.Debug(\"sdk request 'bunny.AddCustomCertificate'\", slog.Any(\"request\", createCertificateReq))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'bunny.AddCustomCertificate': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(apiKey string) (*bunnysdk.Client, error) {\n\treturn bunnysdk.NewClient(apiKey)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/bunny-cdn/bunny_cdn_test.go",
    "content": "package bunnycdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/bunny-cdn\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfApiKey        string\n\tfPullZoneId    string\n\tfHostName      string\n)\n\nfunc init() {\n\targsPrefix := \"BUNNYCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n\tflag.StringVar(&fPullZoneId, argsPrefix+\"PULLZONEID\", \"\", \"\")\n\tflag.StringVar(&fHostName, argsPrefix+\"HOSTNAME\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./bunny_cdn_test.go -args \\\n\t--BUNNYCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--BUNNYCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--BUNNYCDN_APITOKEN=\"your-api-token\" \\\n\t--BUNNYCDN_PULLZONEID=\"your-pull-zone-id\" \\\n\t--BUNNYCDN_HOSTNAME=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t\tfmt.Sprintf(\"PULLZONEID: %v\", fPullZoneId),\n\t\t\tfmt.Sprintf(\"HOSTNAME: %v\", fHostName),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tApiKey:     fApiKey,\n\t\t\tPullZoneId: fPullZoneId,\n\t\t\tHostname:   fHostName,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/byteplus-cdn/byteplus_cdn.go",
    "content": "package bytepluscdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\tbpcdn \"github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn\"\n\tbp \"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/byteplus-cdn\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// BytePlus AccessKey。\n\tAccessKey string `json:\"accessKey\"`\n\t// BytePlus SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *bpcdn.CDN\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient := bpcdn.NewInstance()\n\tclient.Client.SetAccessKey(config.AccessKey)\n\tclient.Client.SetSecretKey(config.SecretKey)\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKey: config.AccessKey,\n\t\tSecretKey: config.SecretKey,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tdomains := make([]string, 0)\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getMatchedDomainsByWildcard(ctx, d.config.Domain)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = domainCandidates\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tdomainCandidates, err := d.getMatchedDomainsByCertId(ctx, upres.CertId)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = domainCandidates\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历绑定证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no cdn domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found cdn domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getMatchedDomainsByWildcard(ctx context.Context, wildcardDomain string) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询加速域名列表，获取匹配的域名\n\t// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/ListCdnDomains_en-us\n\tlistCdnDomainsPageNum := 1\n\tlistCdnDomainsPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistCdnDomainsReq := &bpcdn.ListCdnDomainsRequest{\n\t\t\tDomain:   bp.String(strings.TrimPrefix(wildcardDomain, \"*.\")),\n\t\t\tStatus:   bp.String(\"online\"),\n\t\t\tPageNum:  bp.Int64(int64(listCdnDomainsPageNum)),\n\t\t\tPageSize: bp.Int64(int64(listCdnDomainsPageSize)),\n\t\t}\n\t\tlistCdnDomainsResp, err := d.sdkClient.ListCdnDomains(listCdnDomainsReq)\n\t\td.logger.Debug(\"sdk request 'cdn.ListCdnDomains'\", slog.Any(\"request\", listCdnDomainsReq), slog.Any(\"response\", listCdnDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.ListCdnDomains': %w\", err)\n\t\t}\n\n\t\tfor _, domainItem := range listCdnDomainsResp.Result.Data {\n\t\t\tif xcerthostname.IsMatch(wildcardDomain, domainItem.Domain) {\n\t\t\t\tdomains = append(domains, domainItem.Domain)\n\t\t\t}\n\t\t}\n\n\t\tif len(listCdnDomainsResp.Result.Data) < listCdnDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tlistCdnDomainsPageSize++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) getMatchedDomainsByCertId(ctx context.Context, cloudCertId string) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 获取指定证书可关联的域名\n\t// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-describecertconfig-9ea17\n\tdescribeCertConfigReq := &bpcdn.DescribeCertConfigRequest{\n\t\tCertId: cloudCertId,\n\t}\n\tdescribeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq)\n\td.logger.Debug(\"sdk request 'cdn.DescribeCertConfig'\", slog.Any(\"request\", describeCertConfigReq), slog.Any(\"response\", describeCertConfigResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.DescribeCertConfig': %w\", err)\n\t}\n\n\tif describeCertConfigResp.Result.CertNotConfig != nil {\n\t\tfor i := range describeCertConfigResp.Result.CertNotConfig {\n\t\t\tdomains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain)\n\t\t}\n\t}\n\n\tif describeCertConfigResp.Result.OtherCertConfig != nil {\n\t\tfor i := range describeCertConfigResp.Result.OtherCertConfig {\n\t\t\tdomains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain)\n\t\t}\n\t}\n\n\tif len(domains) == 0 {\n\t\tif len(describeCertConfigResp.Result.SpecifiedCertConfig) == 0 {\n\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t}\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error {\n\t// 关联证书与加速域名\n\t// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-batchdeploycert\n\tbatchDeployCertReq := &bpcdn.BatchDeployCertRequest{\n\t\tCertId: cloudCertId,\n\t\tDomain: domain,\n\t}\n\tbatchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)\n\td.logger.Debug(\"sdk request 'cdn.BatchDeployCert'\", slog.Any(\"request\", batchDeployCertReq), slog.Any(\"response\", batchDeployCertResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.BatchDeployCert': %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/byteplus-cdn/byteplus_cdn_test.go",
    "content": "package bytepluscdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/byteplus-cdn\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfAccessKey     string\n\tfSecretKey     string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"BYTEPLUSCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKey, argsPrefix+\"ACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./byteplus_cdn_test.go -args \\\n\t--BYTEPLUSCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--BYTEPLUSCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--BYTEPLUSCDN_ACCESSKEY=\"your-access-key\" \\\n\t--BYTEPLUSCDN_SECRETKEY=\"your-secret-key\" \\\n\t--BYTEPLUSCDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEY: %v\", fAccessKey),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKey: fAccessKey,\n\t\t\tSecretKey: fSecretKey,\n\t\t\tDomain:    fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/byteplus-cdn/consts.go",
    "content": "package bytepluscdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/cachefly/cachefly.go",
    "content": "package cachefly\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tcacheflysdk \"github.com/certimate-go/certimate/pkg/sdk3rd/cachefly\"\n)\n\ntype DeployerConfig struct {\n\t// CacheFly API Token。\n\tApiToken string `json:\"apiToken\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *cacheflysdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ApiToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\t// REF: https://api.cachefly.com/api/2.5/docs#tag/Certificates/paths/~1certificates/post\n\tcreateCertificateReq := &cacheflysdk.CreateCertificateRequest{\n\t\tCertificate:    lo.ToPtr(certPEM),\n\t\tCertificateKey: lo.ToPtr(privkeyPEM),\n\t}\n\tcreateCertificateResp, err := d.sdkClient.CreateCertificateWithContext(ctx, createCertificateReq)\n\td.logger.Debug(\"sdk request 'cachefly.CreateCertificate'\", slog.Any(\"request\", createCertificateReq), slog.Any(\"response\", createCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cachefly.CreateCertificate': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(apiToken string) (*cacheflysdk.Client, error) {\n\treturn cacheflysdk.NewClient(apiToken)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/cachefly/cachefly_test.go",
    "content": "package cachefly_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/cachefly\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfApiToken      string\n)\n\nfunc init() {\n\targsPrefix := \"CACHEFLY_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fApiToken, argsPrefix+\"APITOKEN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./cachefly_test.go -args \\\n\t--CACHEFLY_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--CACHEFLY_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--CACHEFLY_APITOKEN=\"your-api-token\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"APITOKEN: %v\", fApiToken),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tApiToken: fApiToken,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/cdnfly/cdnfly.go",
    "content": "package cdnfly\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tcdnflysdk \"github.com/certimate-go/certimate/pkg/sdk3rd/cdnfly\"\n)\n\ntype DeployerConfig struct {\n\t// Cdnfly 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// Cdnfly 用户端 API Key。\n\tApiKey string `json:\"apiKey\"`\n\t// Cdnfly 用户端 API Secret。\n\tApiSecret string `json:\"apiSecret\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 网站 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_WEBSITE] 时必填。\n\tSiteId string `json:\"siteId,omitempty\"`\n\t// 证书 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。\n\tCertificateId string `json:\"certificateId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *cdnflysdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiKey, config.ApiSecret, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_WEBSITE:\n\t\tif err := d.deployToSite(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_CERTIFICATE:\n\t\tif err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToSite(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.SiteId == \"\" {\n\t\treturn errors.New(\"config `siteId` is required\")\n\t}\n\n\t// 获取单个网站详情\n\t// REF: https://doc.cdnfly.cn/wangzhanguanli-v1-sites.html#%E8%8E%B7%E5%8F%96%E5%8D%95%E4%B8%AA%E7%BD%91%E7%AB%99%E8%AF%A6%E6%83%85\n\tgetSiteResp, err := d.sdkClient.GetSiteWithContext(ctx, d.config.SiteId)\n\td.logger.Debug(\"sdk request 'cdnfly.GetSite'\", slog.String(\"siteId\", d.config.SiteId), slog.Any(\"response\", getSiteResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdnfly.GetSite': %w\", err)\n\t}\n\n\t// 添加单个证书\n\t// REF: https://doc.cdnfly.cn/wangzhanzhengshu-v1-certs.html#%E6%B7%BB%E5%8A%A0%E5%8D%95%E4%B8%AA%E6%88%96%E5%A4%9A%E4%B8%AA%E8%AF%81%E4%B9%A6-%E5%A4%9A%E4%B8%AA%E8%AF%81%E4%B9%A6%E6%97%B6%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F%E4%B8%BA%E6%95%B0%E7%BB%84\n\tcreateCertificateReq := &cdnflysdk.CreateCertRequest{\n\t\tName: lo.ToPtr(fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli())),\n\t\tType: lo.ToPtr(\"custom\"),\n\t\tCert: lo.ToPtr(certPEM),\n\t\tKey:  lo.ToPtr(privkeyPEM),\n\t}\n\tcreateCertificateResp, err := d.sdkClient.CreateCertWithContext(ctx, createCertificateReq)\n\td.logger.Debug(\"sdk request 'cdnfly.CreateCert'\", slog.Any(\"request\", createCertificateReq), slog.Any(\"response\", createCertificateResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdnfly.CreateCert': %w\", err)\n\t}\n\n\t// 修改单个网站\n\t// REF: https://doc.cdnfly.cn/wangzhanguanli-v1-sites.html#%E4%BF%AE%E6%94%B9%E5%8D%95%E4%B8%AA%E7%BD%91%E7%AB%99\n\tupdateSiteHttpsListenMap := make(map[string]any)\n\t_ = json.Unmarshal([]byte(getSiteResp.Data.HttpsListen), &updateSiteHttpsListenMap)\n\tupdateSiteHttpsListenMap[\"cert\"] = createCertificateResp.Data\n\tupdateSiteHttpsListenData, _ := json.Marshal(updateSiteHttpsListenMap)\n\tupdateSiteReq := &cdnflysdk.UpdateSiteRequest{\n\t\tHttpsListen: lo.ToPtr(string(updateSiteHttpsListenData)),\n\t}\n\tupdateSiteResp, err := d.sdkClient.UpdateSiteWithContext(ctx, d.config.SiteId, updateSiteReq)\n\td.logger.Debug(\"sdk request 'cdnfly.UpdateSite'\", slog.String(\"siteId\", d.config.SiteId), slog.Any(\"request\", updateSiteReq), slog.Any(\"response\", updateSiteResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdnfly.UpdateSite': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.CertificateId == \"\" {\n\t\treturn errors.New(\"config `certificateId` is required\")\n\t}\n\n\t// 修改单个证书\n\t// REF: https://doc.cdnfly.cn/wangzhanzhengshu-v1-certs.html#%E4%BF%AE%E6%94%B9%E5%8D%95%E4%B8%AA%E8%AF%81%E4%B9%A6\n\tupdateCertReq := &cdnflysdk.UpdateCertRequest{\n\t\tType: lo.ToPtr(\"custom\"),\n\t\tCert: lo.ToPtr(certPEM),\n\t\tKey:  lo.ToPtr(privkeyPEM),\n\t}\n\tupdateCertResp, err := d.sdkClient.UpdateCertWithContext(ctx, d.config.CertificateId, updateCertReq)\n\td.logger.Debug(\"sdk request 'cdnfly.UpdateCert'\", slog.String(\"certId\", d.config.CertificateId), slog.Any(\"request\", updateCertReq), slog.Any(\"response\", updateCertResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdnfly.UpdateCert': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(serverUrl, apiKey, apiSecret string, skipTlsVerify bool) (*cdnflysdk.Client, error) {\n\tclient, err := cdnflysdk.NewClient(serverUrl, apiKey, apiSecret)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/cdnfly/cdnfly_test.go",
    "content": "package cdnfly_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/cdnfly\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfApiKey        string\n\tfApiSecret     string\n\tfCertificateId string\n)\n\nfunc init() {\n\targsPrefix := \"CDNFLY_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n\tflag.StringVar(&fApiSecret, argsPrefix+\"APISECRET\", \"\", \"\")\n\tflag.StringVar(&fCertificateId, argsPrefix+\"CERTIFICATEID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./cdnfly_test.go -args \\\n\t--CDNFLY_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--CDNFLY_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--CDNFLY_SERVERURL=\"http://127.0.0.1:88\" \\\n\t--CDNFLY_APIKEY=\"your-api-key\" \\\n\t--CDNFLY_APISECRET=\"your-api-secret\" \\\n\t--CDNFLY_CERTIFICATEID=\"your-cert-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t\tfmt.Sprintf(\"APISECRET: %v\", fApiSecret),\n\t\t\tfmt.Sprintf(\"CERTIFICATEID: %v\", fCertificateId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiKey:                   fApiKey,\n\t\t\tApiSecret:                fApiSecret,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tResourceType:             provider.RESOURCE_TYPE_CERTIFICATE,\n\t\t\tCertificateId:            fCertificateId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/cdnfly/consts.go",
    "content": "package cdnfly\n\nconst (\n\t// 资源类型：替换指定网站的证书。\n\tRESOURCE_TYPE_WEBSITE = \"website\"\n\t// 资源类型：替换指定证书。\n\tRESOURCE_TYPE_CERTIFICATE = \"certificate\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/cpanel/consts.go",
    "content": "package cpanel\n\nconst (\n\t// 资源类型：替换指定网站的证书。\n\tRESOURCE_TYPE_WEBSITE = \"website\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/cpanel/cpanel.go",
    "content": "package cpanel\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tcpanelsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/cpanel\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype DeployerConfig struct {\n\t// cPanel 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// cPanel 用户名。\n\tUsername string `json:\"username\"`\n\t// cPanel 接口密钥。\n\tApiToken string `json:\"apiToken\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 网站域名（不支持泛域名）。\n\t// 部署资源类型为 [RESOURCE_TYPE_WEBSITE] 时必填。\n\tDomain string `json:\"domain,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *cpanelsdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.Username, config.ApiToken, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_WEBSITE:\n\t\tif err := d.deployToWebsite(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToWebsite(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.Domain == \"\" {\n\t\treturn errors.New(\"config `domain` is required\")\n\t}\n\n\t// 提取服务器证书和中间证书\n\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to extract certs: %w\", err)\n\t}\n\n\t// 安装 SSL 证书\n\t// REF: https://api.docs.cpanel.net/openapi/cpanel/operation/install_ssl/\n\tsslInstallSSLReq := &cpanelsdk.SSLInstallSSLRequest{\n\t\tDomain:   lo.ToPtr(d.config.Domain),\n\t\tCert:     lo.ToPtr(serverCertPEM),\n\t\tKey:      lo.ToPtr(privkeyPEM),\n\t\tCABundle: lo.ToPtr(intermediaCertPEM),\n\t}\n\tsslInstallSSLResp, err := d.sdkClient.SSLInstallSSL(sslInstallSSLReq)\n\td.logger.Debug(\"sdk request 'SSL.install_ssl'\", slog.Any(\"request\", sslInstallSSLReq), slog.Any(\"response\", sslInstallSSLResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'SSL.install_ssl': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(serverUrl, username, apiToken string, skipTlsVerify bool) (*cpanelsdk.Client, error) {\n\tclient, err := cpanelsdk.NewClient(serverUrl, username, apiToken)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/cpanel/cpanel_test.go",
    "content": "package cpanel_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/cpanel\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfUsername      string\n\tfApiToken      string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"CPANEL_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fUsername, argsPrefix+\"USERNAME\", \"\", \"\")\n\tflag.StringVar(&fApiToken, argsPrefix+\"APITOKEN\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./cpanel_test.go -args \\\n\t--CPANEL_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--CPANEL_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--CPANEL_SERVERURL=\"http://127.0.0.1:2082\" \\\n\t--CPANEL_USERNAME=\"your-username\" \\\n\t--CPANEL_APITOKEN=\"your-api-token\" \\\n\t--CPANEL_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"USERNAME: %v\", fUsername),\n\t\t\tfmt.Sprintf(\"APITOKEN: %v\", fApiToken),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tUsername:                 fUsername,\n\t\t\tApiToken:                 fApiToken,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tResourceType:             provider.RESOURCE_TYPE_WEBSITE,\n\t\t\tDomain:                   fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-ao/consts.go",
    "content": "package ctcccloudao\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao.go",
    "content": "package ctcccloudao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-ao\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tctyunao \"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/ao\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 天翼云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 天翼云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *ctyunao.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tSecretAccessKey: config.SecretAccessKey,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain)\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no accessone domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found accessone domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, upres.CertName); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13816&data=174&isNormal=1&vid=167\n\tqueryDomainsPage := 1\n\tqueryDomainsPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tqueryDomainsReq := &ctyunao.QueryDomainsRequest{\n\t\t\tPage:        lo.ToPtr(int32(queryDomainsPage)),\n\t\t\tPageSize:    lo.ToPtr(int32(queryDomainsPageSize)),\n\t\t\tProductCode: lo.ToPtr(\"020\"),\n\t\t}\n\t\tqueryDomainsResp, err := d.sdkClient.QueryDomainsWithContext(ctx, queryDomainsReq)\n\t\td.logger.Debug(\"sdk request 'cdn.QueryDomains'\", slog.Any(\"request\", queryDomainsReq), slog.Any(\"response\", queryDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.QueryDomains': %w\", err)\n\t\t}\n\n\t\tif queryDomainsResp.ReturnObj == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tignoredStatuses := []int32{1, 5, 6, 7, 8, 9, 11, 12}\n\t\tfor _, domainItem := range queryDomainsResp.ReturnObj.Results {\n\t\t\tif lo.Contains(ignoredStatuses, domainItem.Status) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdomains = append(domains, domainItem.Domain)\n\t\t}\n\n\t\tif len(queryDomainsResp.ReturnObj.Results) < queryDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tqueryDomainsPage++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertName string) error {\n\t// 域名基础及加速配置查询\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13412&data=174&isNormal=1&vid=167\n\tgetDomainConfigReq := &ctyunao.GetDomainConfigRequest{\n\t\tDomain:      lo.ToPtr(domain),\n\t\tProductCode: lo.ToPtr(\"020\"),\n\t}\n\tgetDomainConfigResp, err := d.sdkClient.GetDomainConfigWithContext(ctx, getDomainConfigReq)\n\td.logger.Debug(\"sdk request 'cdn.GetDomainConfig'\", slog.Any(\"request\", getDomainConfigReq), slog.Any(\"response\", getDomainConfigResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.GetDomainConfig': %w\", err)\n\t}\n\n\t// 域名基础及加速配置修改\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13413&data=174&isNormal=1&vid=167\n\tmodifyDomainConfigReq := &ctyunao.ModifyDomainConfigRequest{\n\t\tDomain:      lo.ToPtr(domain),\n\t\tProductCode: lo.ToPtr(getDomainConfigResp.ReturnObj.ProductCode),\n\t\tOrigin: lo.Map(getDomainConfigResp.ReturnObj.Origin, func(item *ctyunao.DomainOriginConfigWithWeight, _ int) *ctyunao.DomainOriginConfig {\n\t\t\tweight := item.Weight\n\t\t\tif weight == 0 {\n\t\t\t\tweight = 1\n\t\t\t}\n\t\t\treturn &ctyunao.DomainOriginConfig{\n\t\t\t\tOrigin: item.Origin,\n\t\t\t\tRole:   item.Role,\n\t\t\t\tWeight: strconv.Itoa(int(weight)),\n\t\t\t}\n\t\t}),\n\t\tHttpsStatus: lo.ToPtr(\"on\"),\n\t\tCertName:    lo.ToPtr(cloudCertName),\n\t}\n\tmodifyDomainConfigResp, err := d.sdkClient.ModifyDomainConfigWithContext(ctx, modifyDomainConfigReq)\n\td.logger.Debug(\"sdk request 'cdn.ModifyDomainConfig'\", slog.Any(\"request\", modifyDomainConfigReq), slog.Any(\"response\", modifyDomainConfigResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.ModifyDomainConfig': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey string) (*ctyunao.Client, error) {\n\treturn ctyunao.NewClient(accessKeyId, secretAccessKey)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao_test.go",
    "content": "package ctcccloudao_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-ao\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"CTCCCLOUDAO_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ctcccloud_ao_test.go -args \\\n\t--CTCCCLOUDAO_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--CTCCCLOUDAO_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--CTCCCLOUDAO_ACCESSKEYID=\"your-access-key-id\" \\\n\t--CTCCCLOUDAO_SECRETACCESSKEY=\"your-secret-access-key\" \\\n\t--CTCCCLOUDAO_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tSecretAccessKey:    fSecretAccessKey,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-cdn/consts.go",
    "content": "package ctcccloudcdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-cdn/ctcccloud_cdn.go",
    "content": "package ctcccloudcdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-cdn\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tctyuncdn \"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/cdn\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 天翼云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 天翼云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *ctyuncdn.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tSecretAccessKey: config.SecretAccessKey,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain)\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no cdn domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found cdn domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, upres.CertName); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11307&data=161&isNormal=1&vid=154\n\tqueryDomainListPage := 1\n\tqueryDomainListPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tqueryDomainListReq := &ctyuncdn.QueryDomainListRequest{\n\t\t\tPage:        lo.ToPtr(int32(queryDomainListPage)),\n\t\t\tPageSize:    lo.ToPtr(int32(queryDomainListPageSize)),\n\t\t\tProductCode: lo.ToPtr(\"020\"),\n\t\t}\n\t\tqueryDomainListResp, err := d.sdkClient.QueryDomainListWithContext(ctx, queryDomainListReq)\n\t\td.logger.Debug(\"sdk request 'cdn.QueryDomainList'\", slog.Any(\"request\", queryDomainListReq), slog.Any(\"response\", queryDomainListResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.QueryDomainList': %w\", err)\n\t\t}\n\n\t\tif queryDomainListResp.ReturnObj == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfilteredProductCodes := []string{\"001\", \"003\", \"004\", \"008\"}\n\t\tignoredStatuses := []int32{1, 5, 6, 7, 8, 9, 11, 12}\n\t\tfor _, domainItem := range queryDomainListResp.ReturnObj.Results {\n\t\t\tif !lo.Contains(filteredProductCodes, domainItem.ProductCode) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif lo.Contains(ignoredStatuses, domainItem.Status) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdomains = append(domains, domainItem.Domain)\n\t\t}\n\n\t\tif len(queryDomainListResp.ReturnObj.Results) < queryDomainListPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tqueryDomainListPage++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertName string) error {\n\t// 查询域名配置信息\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11304&data=161&isNormal=1&vid=154\n\tqueryDomainDetailReq := &ctyuncdn.QueryDomainDetailRequest{\n\t\tDomain: lo.ToPtr(domain),\n\t}\n\tqueryDomainDetailResp, err := d.sdkClient.QueryDomainDetailWithContext(ctx, queryDomainDetailReq)\n\td.logger.Debug(\"sdk request 'cdn.QueryDomainDetail'\", slog.Any(\"request\", queryDomainDetailReq), slog.Any(\"response\", queryDomainDetailResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.QueryDomainDetail': %w\", err)\n\t}\n\n\t// 修改域名配置\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11308&data=161&isNormal=1&vid=154\n\tupdateDomainReq := &ctyuncdn.UpdateDomainRequest{\n\t\tDomain:      lo.ToPtr(domain),\n\t\tHttpsStatus: lo.ToPtr(\"on\"),\n\t\tCertName:    lo.ToPtr(cloudCertName),\n\t}\n\tupdateDomainResp, err := d.sdkClient.UpdateDomainWithContext(ctx, updateDomainReq)\n\td.logger.Debug(\"sdk request 'cdn.UpdateDomain'\", slog.Any(\"request\", updateDomainReq), slog.Any(\"response\", updateDomainResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.UpdateDomain': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey string) (*ctyuncdn.Client, error) {\n\treturn ctyuncdn.NewClient(accessKeyId, secretAccessKey)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-cdn/ctcccloud_cdn_test.go",
    "content": "package ctcccloudcdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-cdn\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"CTCCCLOUDCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ctcccloud_cdn_test.go -args \\\n\t--CTCCCLOUDCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--CTCCCLOUDCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--CTCCCLOUDCDN_ACCESSKEYID=\"your-access-key-id\" \\\n\t--CTCCCLOUDCDN_SECRETACCESSKEY=\"your-secret-access-key\" \\\n\t--CTCCCLOUDCDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tSecretAccessKey:    fSecretAccessKey,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-cms/ctcccloud_cms.go",
    "content": "package ctcccloudcms\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-cms\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// 天翼云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 天翼云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tSecretAccessKey: config.SecretAccessKey,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-cms/ctcccloud_cms_test.go",
    "content": "package ctcccloudcms_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-cms\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n)\n\nfunc init() {\n\targsPrefix := \"CTCCCLOUDCMS_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ctcccloud_cms_test.go -args \\\n\t--CTCCCLOUDCMS_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--CTCCCLOUDCMS_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--CTCCCLOUDCMS_ACCESSKEYID=\"your-access-key-id\" \\\n\t--CTCCCLOUDCMS_SECRETACCESSKEY=\"your-secret-access-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-elb/consts.go",
    "content": "package ctcccloudelb\n\nconst (\n\t// 资源类型：部署到指定负载均衡器。\n\tRESOURCE_TYPE_LOADBALANCER = \"loadbalancer\"\n\t// 资源类型：部署到指定监听器。\n\tRESOURCE_TYPE_LISTENER = \"listener\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-elb/ctcccloud_elb.go",
    "content": "package ctcccloudelb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-elb\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tctyunelb \"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/elb\"\n)\n\ntype DeployerConfig struct {\n\t// 天翼云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 天翼云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 天翼云资源池 ID。\n\tRegionId string `json:\"regionId\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 负载均衡实例 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。\n\tLoadbalancerId string `json:\"loadbalancerId,omitempty\"`\n\t// 负载均衡监听器 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。\n\tListenerId string `json:\"listenerId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *ctyunelb.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tSecretAccessKey: config.SecretAccessKey,\n\t\tRegionId:        config.RegionId,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_LOADBALANCER:\n\t\tif err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_LISTENER:\n\t\tif err := d.deployToListener(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\n\t// 查询监听列表\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5654&data=88&isNormal=1&vid=82\n\tlistenerIds := make([]string, 0)\n\t{\n\t\tlistListenersReq := &ctyunelb.ListListenersRequest{\n\t\t\tRegionID:       lo.ToPtr(d.config.RegionId),\n\t\t\tLoadBalancerID: lo.ToPtr(d.config.LoadbalancerId),\n\t\t}\n\t\tlistListenersResp, err := d.sdkClient.ListListenersWithContext(ctx, listListenersReq)\n\t\td.logger.Debug(\"sdk request 'elb.ListListeners'\", slog.Any(\"request\", listListenersReq), slog.Any(\"response\", listListenersResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'elb.ListListeners': %w\", err)\n\t\t}\n\n\t\tfor _, listener := range listListenersResp.ReturnObj {\n\t\t\tif strings.EqualFold(listener.Protocol, \"HTTPS\") {\n\t\t\t\tlistenerIds = append(listenerIds, listener.ID)\n\t\t\t}\n\t\t}\n\t}\n\n\t// 遍历更新监听证书\n\tif len(listenerIds) == 0 {\n\t\td.logger.Info(\"no elb listeners to deploy\")\n\t} else {\n\t\td.logger.Info(\"found https listeners to deploy\", slog.Any(\"listenerIds\", listenerIds))\n\t\tvar errs []error\n\n\t\tfor _, listenerId := range listenerIds {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error {\n\tif d.config.ListenerId == \"\" {\n\t\treturn errors.New(\"config `listenerId` is required\")\n\t}\n\n\t// 更新监听\n\tif err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {\n\t// 更新监听器\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5652&data=88&isNormal=1&vid=82\n\tsetLoadBalancerHTTPSListenerAttributeReq := &ctyunelb.UpdateListenerRequest{\n\t\tRegionID:      lo.ToPtr(d.config.RegionId),\n\t\tListenerID:    lo.ToPtr(cloudListenerId),\n\t\tCertificateID: lo.ToPtr(cloudCertId),\n\t}\n\tsetLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.UpdateListenerWithContext(ctx, setLoadBalancerHTTPSListenerAttributeReq)\n\td.logger.Debug(\"sdk request 'elb.UpdateListener'\", slog.Any(\"request\", setLoadBalancerHTTPSListenerAttributeReq), slog.Any(\"response\", setLoadBalancerHTTPSListenerAttributeResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'elb.UpdateListener': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey string) (*ctyunelb.Client, error) {\n\treturn ctyunelb.NewClient(accessKeyId, secretAccessKey)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-elb/ctcccloud_elb_test.go",
    "content": "package ctcccloudelb_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-elb\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n\tfRegionId        string\n\tfLoadbalancerId  string\n\tfListenerId      string\n)\n\nfunc init() {\n\targsPrefix := \"CTCCCLOUDELB_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fRegionId, argsPrefix+\"REGIONID\", \"\", \"\")\n\tflag.StringVar(&fLoadbalancerId, argsPrefix+\"LOADBALANCERID\", \"\", \"\")\n\tflag.StringVar(&fListenerId, argsPrefix+\"LISTENERID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ctcccloud_elb_test.go -args \\\n\t--CTCCCLOUDELB_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--CTCCCLOUDELB_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--CTCCCLOUDELB_ACCESSKEYID=\"your-access-key-id\" \\\n\t--CTCCCLOUDELB_SECRETACCESSKEY=\"your-secret-access-key\" \\\n\t--CTCCCLOUDELB_REGIONID=\"your-region-id\" \\\n\t--CTCCCLOUDELB_LOADBALANCERID=\"your-elb-instance-id\" \\\n\t--CTCCCLOUDELB_LISTENERID=\"your-elb-listener-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy_ToLoadbalancer\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"REGIONID: %v\", fRegionId),\n\t\t\tfmt.Sprintf(\"LOADBALANCERID: %v\", fLoadbalancerId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t\tRegionId:        fRegionId,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LOADBALANCER,\n\t\t\tLoadbalancerId:  fLoadbalancerId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n\n\tt.Run(\"Deploy_ToListener\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"REGIONID: %v\", fRegionId),\n\t\t\tfmt.Sprintf(\"LISTENERID: %v\", fListenerId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t\tRegionId:        fRegionId,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LISTENER,\n\t\t\tListenerId:      fListenerId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-faas/ctcccloud_faas.go",
    "content": "package ctcccloudfaas\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tctyunfaas \"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/faas\"\n)\n\ntype DeployerConfig struct {\n\t// 天翼云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 天翼云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 天翼云资源池 ID。\n\tRegionId string `json:\"regionId\"`\n\t// 自定义域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *ctyunfaas.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.RegionId == \"\" {\n\t\treturn nil, errors.New(\"config `regionId` is required\")\n\t}\n\tif d.config.Domain == \"\" {\n\t\treturn nil, errors.New(\"config `domain` is required\")\n\t}\n\n\t// 获取自定义域名配置\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=53&api=16002&data=42&isNormal=1&vid=40\n\tvar faasCustomDomain *ctyunfaas.CustomDomainRecord\n\tgetCustomDomainReq := &ctyunfaas.GetCustomDomainRequest{\n\t\tRegionId:   lo.ToPtr(d.config.RegionId),\n\t\tDomainName: lo.ToPtr(d.config.Domain),\n\t\tCnameCheck: lo.ToPtr(false),\n\t}\n\tgetCustomDomainResp, err := d.sdkClient.GetCustomDomainWithContext(ctx, getCustomDomainReq)\n\td.logger.Debug(\"sdk request 'faas.GetCustomDomain'\", slog.Any(\"request\", getCustomDomainReq), slog.Any(\"response\", getCustomDomainResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'faas.GetCustomDomain': %w\", err)\n\t} else {\n\t\tfaasCustomDomain = getCustomDomainResp.ReturnObj\n\n\t\t// 已部署过此域名，跳过\n\t\tif faasCustomDomain.CertConfig != nil &&\n\t\t\tfaasCustomDomain.CertConfig.Certificate == certPEM &&\n\t\t\tfaasCustomDomain.CertConfig.PrivateKey == privkeyPEM {\n\t\t\treturn &deployer.DeployResult{}, nil\n\t\t}\n\t}\n\n\t// 更新自定义域名\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=53&api=16004&data=42&isNormal=1&vid=40\n\tupdateCustomDomainReq := &ctyunfaas.UpdateCustomDomainRequest{\n\t\tRegionId:   lo.ToPtr(d.config.RegionId),\n\t\tDomainName: lo.ToPtr(d.config.Domain),\n\t\tProtocol:   lo.ToPtr(faasCustomDomain.Protocol),\n\t\tAuthConfig: faasCustomDomain.AuthConfig,\n\t\tCertConfig: &ctyunfaas.CustomDomainCertConfig{\n\t\t\tCertName:    fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli()),\n\t\t\tCertificate: certPEM,\n\t\t\tPrivateKey:  privkeyPEM,\n\t\t},\n\t}\n\tif !strings.Contains(*updateCustomDomainReq.Protocol, \"HTTPS\") {\n\t\tif *updateCustomDomainReq.Protocol == \"\" {\n\t\t\tupdateCustomDomainReq.Protocol = lo.ToPtr(\"HTTPS\")\n\t\t} else {\n\t\t\tupdateCustomDomainReq.Protocol = lo.ToPtr(*updateCustomDomainReq.Protocol + \",HTTPS\")\n\t\t}\n\t}\n\tupdateCustomDomainResp, err := d.sdkClient.UpdateCustomDomainWithContext(ctx, updateCustomDomainReq)\n\td.logger.Debug(\"sdk request 'faas.UpdateCustomDomain'\", slog.Any(\"request\", updateCustomDomainReq), slog.Any(\"response\", updateCustomDomainResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'faas.UpdateCustomDomain': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey string) (*ctyunfaas.Client, error) {\n\treturn ctyunfaas.NewClient(accessKeyId, secretAccessKey)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-faas/ctcccloud_faas_test.go",
    "content": "package ctcccloudfaas_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-faas\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n\tfRegionId        string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"CTCCCLOUDFAAS_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fRegionId, argsPrefix+\"REGIONID\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ctcccloud_faas_test.go -args \\\n\t--CTCCCLOUDFAAS_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--CTCCCLOUDFAAS_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--CTCCCLOUDFAAS_ACCESSKEYID=\"your-access-key-id\" \\\n\t--CTCCCLOUDFAAS_SECRETACCESSKEY=\"your-secret-access-key\" \\\n\t--CTCCCLOUDFAAS_REGIONID=\"your-region-id\" \\\n\t--CTCCCLOUDFAAS_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"REGIONID: %v\", fRegionId),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t\tRegionId:        fRegionId,\n\t\t\tDomain:          fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-icdn/consts.go",
    "content": "package ctcccloudicdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-icdn/ctcccloud_icdn.go",
    "content": "package ctcccloudicdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-icdn\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tctyunicdn \"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/icdn\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 天翼云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 天翼云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *ctyunicdn.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tSecretAccessKey: config.SecretAccessKey,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain)\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no icdn domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found icdn domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, upres.CertName); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10852&data=173&isNormal=1&vid=166\n\tqueryDomainsPage := 1\n\tqueryDomainsPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tqueryDomainListReq := &ctyunicdn.QueryDomainListRequest{\n\t\t\tPage:        lo.ToPtr(int32(queryDomainsPage)),\n\t\t\tPageSize:    lo.ToPtr(int32(queryDomainsPageSize)),\n\t\t\tProductCode: lo.ToPtr(\"006\"),\n\t\t}\n\t\tqueryDomainListResp, err := d.sdkClient.QueryDomainListWithContext(ctx, queryDomainListReq)\n\t\td.logger.Debug(\"sdk request 'cdn.QueryDomainList'\", slog.Any(\"request\", queryDomainListReq), slog.Any(\"response\", queryDomainListResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.QueryDomainList': %w\", err)\n\t\t}\n\n\t\tif queryDomainListResp.ReturnObj == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tignoredStatuses := []int32{1, 5, 6, 7, 8, 9, 11, 12}\n\t\tfor _, domainItem := range queryDomainListResp.ReturnObj.Results {\n\t\t\tif lo.Contains(ignoredStatuses, domainItem.Status) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdomains = append(domains, domainItem.Domain)\n\t\t}\n\n\t\tif len(queryDomainListResp.ReturnObj.Results) < queryDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tqueryDomainsPage++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertName string) error {\n\t// 查询域名配置信息\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10849&data=173&isNormal=1&vid=166\n\tqueryDomainDetailReq := &ctyunicdn.QueryDomainDetailRequest{\n\t\tDomain: lo.ToPtr(domain),\n\t}\n\tqueryDomainDetailResp, err := d.sdkClient.QueryDomainDetailWithContext(ctx, queryDomainDetailReq)\n\td.logger.Debug(\"sdk request 'icdn.QueryDomainDetail'\", slog.Any(\"request\", queryDomainDetailReq), slog.Any(\"response\", queryDomainDetailResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'icdn.QueryDomainDetail': %w\", err)\n\t}\n\n\t// 修改域名配置\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10853&data=173&isNormal=1&vid=166\n\tupdateDomainReq := &ctyunicdn.UpdateDomainRequest{\n\t\tDomain:      lo.ToPtr(domain),\n\t\tHttpsStatus: lo.ToPtr(\"on\"),\n\t\tCertName:    lo.ToPtr(cloudCertName),\n\t}\n\tupdateDomainResp, err := d.sdkClient.UpdateDomainWithContext(ctx, updateDomainReq)\n\td.logger.Debug(\"sdk request 'icdn.UpdateDomain'\", slog.Any(\"request\", updateDomainReq), slog.Any(\"response\", updateDomainResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'icdn.UpdateDomain': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey string) (*ctyunicdn.Client, error) {\n\treturn ctyunicdn.NewClient(accessKeyId, secretAccessKey)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-icdn/ctcccloud_icdn_test.go",
    "content": "package ctcccloudicdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-icdn\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"CTCCCLOUDCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ctcccloud_cdn_test.go -args \\\n\t--CTCCCLOUDCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--CTCCCLOUDCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--CTCCCLOUDCDN_ACCESSKEYID=\"your-access-key-id\" \\\n\t--CTCCCLOUDCDN_SECRETACCESSKEY=\"your-secret-access-key\" \\\n\t--CTCCCLOUDCDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tSecretAccessKey:    fSecretAccessKey,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-lvdn/consts.go",
    "content": "package ctcccloudlvdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn.go",
    "content": "package ctcccloudlvdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-lvdn\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tctyunlvdn \"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/lvdn\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype DeployerConfig struct {\n\t// 天翼云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 天翼云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *ctyunlvdn.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tSecretAccessKey: config.SecretAccessKey,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no lvdn domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found lvdn domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, upres.CertName); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11559&data=183&isNormal=1&vid=261\n\tqueryDomainsPage := 1\n\tqueryDomainsPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tqueryDomainListReq := &ctyunlvdn.QueryDomainListRequest{\n\t\t\tPage:        lo.ToPtr(int32(queryDomainsPage)),\n\t\t\tPageSize:    lo.ToPtr(int32(queryDomainsPageSize)),\n\t\t\tProductCode: lo.ToPtr(\"005\"),\n\t\t}\n\t\tqueryDomainListResp, err := d.sdkClient.QueryDomainListWithContext(ctx, queryDomainListReq)\n\t\td.logger.Debug(\"sdk request 'cdn.QueryDomainList'\", slog.Any(\"request\", queryDomainListReq), slog.Any(\"response\", queryDomainListResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.QueryDomainList': %w\", err)\n\t\t}\n\n\t\tif queryDomainListResp.ReturnObj == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tignoredStatuses := []int32{1, 5, 6, 7, 8, 9, 11, 12}\n\t\tfor _, domainItem := range queryDomainListResp.ReturnObj.Results {\n\t\t\tif lo.Contains(ignoredStatuses, domainItem.Status) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdomains = append(domains, domainItem.Domain)\n\t\t}\n\n\t\tif len(queryDomainListResp.ReturnObj.Results) < queryDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tqueryDomainsPage++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertName string) error {\n\t// 查询域名配置信息\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11473&data=183&isNormal=1&vid=261\n\tqueryDomainDetailReq := &ctyunlvdn.QueryDomainDetailRequest{\n\t\tDomain:      lo.ToPtr(domain),\n\t\tProductCode: lo.ToPtr(\"005\"),\n\t}\n\tqueryDomainDetailResp, err := d.sdkClient.QueryDomainDetailWithContext(ctx, queryDomainDetailReq)\n\td.logger.Debug(\"sdk request 'lvdn.QueryDomainDetail'\", slog.Any(\"request\", queryDomainDetailReq), slog.Any(\"response\", queryDomainDetailResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'lvdn.QueryDomainDetail': %w\", err)\n\t}\n\n\t// 修改域名配置\n\t// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11308&data=161&isNormal=1&vid=154\n\tupdateDomainReq := &ctyunlvdn.UpdateDomainRequest{\n\t\tDomain:      lo.ToPtr(domain),\n\t\tProductCode: lo.ToPtr(\"005\"),\n\t\tHttpsSwitch: lo.ToPtr(int32(1)),\n\t\tCertName:    lo.ToPtr(cloudCertName),\n\t}\n\tupdateDomainResp, err := d.sdkClient.UpdateDomainWithContext(ctx, updateDomainReq)\n\td.logger.Debug(\"sdk request 'lvdn.UpdateDomain'\", slog.Any(\"request\", updateDomainReq), slog.Any(\"response\", updateDomainResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'lvdn.UpdateDomain': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey string) (*ctyunlvdn.Client, error) {\n\treturn ctyunlvdn.NewClient(accessKeyId, secretAccessKey)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go",
    "content": "package ctcccloudlvdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-lvdn\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"CTCCCLOUDLVDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ctcccloud_lvdn_test.go -args \\\n\t--CTCCCLOUDLVDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--CTCCCLOUDLVDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--CTCCCLOUDLVDN_ACCESSKEYID=\"your-access-key-id\" \\\n\t--CTCCCLOUDLVDN_SECRETACCESSKEY=\"your-secret-access-key\" \\\n\t--CTCCCLOUDLVDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tSecretAccessKey:    fSecretAccessKey,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/dogecloud-cdn/consts.go",
    "content": "package dogecloudcdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/dogecloud-cdn/dogecloud_cdn.go",
    "content": "package dogecloudcdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/dogecloud\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tdogesdk \"github.com/certimate-go/certimate/pkg/sdk3rd/dogecloud\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype DeployerConfig struct {\n\t// 多吉云 AccessKey。\n\tAccessKey string `json:\"accessKey\"`\n\t// 多吉云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *dogesdk.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKey, config.SecretKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKey: config.AccessKey,\n\t\tSecretKey: config.SecretKey,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no cdn domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found cdn domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tcertId, _ := strconv.ParseInt(upres.CertId, 10, 64)\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, certId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 获取域名列表\n\t// REF: https://docs.dogecloud.com/cdn/api-domain-list\n\tlistCdnDomainResp, err := d.sdkClient.ListCdnDomainWithContext(ctx)\n\td.logger.Debug(\"sdk request 'cdn.ListCdnDomain'\", slog.Any(\"response\", listCdnDomainResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.ListCdnDomain': %w\", err)\n\t}\n\n\tif listCdnDomainResp.Data != nil {\n\t\tignoredStatuses := []string{\"offline\"}\n\t\tfor _, domainItem := range listCdnDomainResp.Data.Domains {\n\t\t\tif lo.Contains(ignoredStatuses, domainItem.Status) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdomains = append(domains, domainItem.Name)\n\t\t}\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId int64) error {\n\t// 绑定证书\n\t// REF: https://docs.dogecloud.com/cdn/api-cert-bind\n\tbindCdnCertReq := &dogesdk.BindCdnCertRequest{\n\t\tCertId: cloudCertId,\n\t\tDomain: domain,\n\t}\n\tbindCdnCertResp, err := d.sdkClient.BindCdnCertWithContext(ctx, bindCdnCertReq)\n\td.logger.Debug(\"sdk request 'cdn.BindCdnCert'\", slog.Any(\"request\", bindCdnCertReq), slog.Any(\"response\", bindCdnCertResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.BindCdnCert': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKey, secretKey string) (*dogesdk.Client, error) {\n\treturn dogesdk.NewClient(accessKey, secretKey)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/dogecloud-cdn/dogecloud_cdn_test.go",
    "content": "package dogecloudcdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/dogecloud-cdn\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfAccessKey     string\n\tfSecretKey     string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"DOGECLOUDCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKey, argsPrefix+\"ACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./dogecloud_cdn_test.go -args \\\n\t--DOGECLOUDCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--DOGECLOUDCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--DOGECLOUDCDN_ACCESSKEY=\"your-access-key\" \\\n\t--DOGECLOUDCDN_SECRETKEY=\"your-secret-key\" \\\n\t--DOGECLOUDCDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEY: %v\", fAccessKey),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKey: fAccessKey,\n\t\t\tSecretKey: fSecretKey,\n\t\t\tDomain:    fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/dokploy/dokploy.go",
    "content": "package dokploy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/dokploy\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// Dokploy 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// Dokploy API Key。\n\tApiKey string `json:\"apiKey\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tServerUrl:                config.ServerUrl,\n\t\tApiKey:                   config.ApiKey,\n\t\tAllowInsecureConnections: config.AllowInsecureConnections,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/dokploy/dokploy_test.go",
    "content": "package dokploy_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/dokploy\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfApiKey        string\n)\n\nfunc init() {\n\targsPrefix := \"DOKPLOY_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./1panel_console_test.go -args \\\n\t--DOKPLOY_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--DOKPLOY_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--DOKPLOY_SERVERURL=\"http://127.0.0.1:3000\" \\\n\t--DOKPLOY_APIKEY=\"your-api-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiKey:                   fApiKey,\n\t\t\tAllowInsecureConnections: true,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/flexcdn/consts.go",
    "content": "package flexcdn\n\nconst (\n\t// 资源类型：替换指定证书。\n\tRESOURCE_TYPE_CERTIFICATE = \"certificate\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/flexcdn/flexcdn.go",
    "content": "package flexcdn\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tflexcdnsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/flexcdn\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype DeployerConfig struct {\n\t// FlexCDN 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// FlexCDN 用户角色。\n\t// 可取值 \"user\"、\"admin\"。\n\tApiRole string `json:\"apiRole\"`\n\t// FlexCDN AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// FlexCDN AccessKey。\n\tAccessKey string `json:\"accessKey\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 证书 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。\n\tCertificateId int64 `json:\"certificateId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *flexcdnsdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiRole, config.AccessKeyId, config.AccessKey, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_CERTIFICATE:\n\t\tif err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.CertificateId == 0 {\n\t\treturn errors.New(\"config `certificateId` is required\")\n\t}\n\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 修改证书\n\t// REF: https://flexcdn.cloud/dev/api/service/SSLCertService?role=user#updateSSLCert\n\tupdateSSLCertReq := &flexcdnsdk.UpdateSSLCertRequest{\n\t\tSSLCertId:   d.config.CertificateId,\n\t\tIsOn:        true,\n\t\tName:        fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli()),\n\t\tDescription: \"upload from certimate\",\n\t\tServerName:  certX509.Subject.CommonName,\n\t\tIsCA:        false,\n\t\tCertData:    base64.StdEncoding.EncodeToString([]byte(certPEM)),\n\t\tKeyData:     base64.StdEncoding.EncodeToString([]byte(privkeyPEM)),\n\t\tTimeBeginAt: certX509.NotBefore.Unix(),\n\t\tTimeEndAt:   certX509.NotAfter.Unix(),\n\t\tDNSNames:    certX509.DNSNames,\n\t\tCommonNames: []string{certX509.Subject.CommonName},\n\t}\n\tupdateSSLCertResp, err := d.sdkClient.UpdateSSLCertWithContext(ctx, updateSSLCertReq)\n\td.logger.Debug(\"sdk request 'flexcdn.UpdateSSLCert'\", slog.Any(\"request\", updateSSLCertReq), slog.Any(\"response\", updateSSLCertResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'flexcdn.UpdateSSLCert': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(serverUrl, apiRole, accessKeyId, accessKey string, skipTlsVerify bool) (*flexcdnsdk.Client, error) {\n\tclient, err := flexcdnsdk.NewClient(serverUrl, apiRole, accessKeyId, accessKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/flexcdn/flexcdn_test.go",
    "content": "package flexcdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/flexcdn\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfAccessKeyId   string\n\tfAccessKey     string\n\tfCertificateId int64\n)\n\nfunc init() {\n\targsPrefix := \"FLEXCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKey, argsPrefix+\"ACCESSKEY\", \"\", \"\")\n\tflag.Int64Var(&fCertificateId, argsPrefix+\"CERTIFICATEID\", 0, \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./flexcdn_test.go -args \\\n\t--FLEXCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--FLEXCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--FLEXCDN_SERVERURL=\"http://127.0.0.1:7788\" \\\n\t--FLEXCDN_ACCESSKEYID=\"your-access-key-id\" \\\n\t--FLEXCDN_ACCESSKEY=\"your-access-key\" \\\n\t--FLEXCDN_CERTIFICATEID=\"your-certificate-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy_ToCertificate\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEY: %v\", fAccessKey),\n\t\t\tfmt.Sprintf(\"CERTIFICATEID: %v\", fCertificateId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiRole:                  \"user\",\n\t\t\tAccessKeyId:              fAccessKeyId,\n\t\t\tAccessKey:                fAccessKey,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tResourceType:             provider.RESOURCE_TYPE_CERTIFICATE,\n\t\t\tCertificateId:            fCertificateId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/flyio/flyio.go",
    "content": "package flyio\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tflyiosdk \"github.com/certimate-go/certimate/pkg/sdk3rd/flyio\"\n)\n\ntype DeployerConfig struct {\n\t// Fly.io API Token。\n\tApiToken string `json:\"apiToken\"`\n\t// Fly.io 应用名称。\n\tAppName string `json:\"appName\"`\n\t// 自定义域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *flyiosdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ApiToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.AppName == \"\" {\n\t\treturn nil, errors.New(\"config `appName` is required\")\n\t}\n\tif d.config.Domain == \"\" {\n\t\treturn nil, errors.New(\"config `domain` is required\")\n\t}\n\n\t// 导入自定义证书\n\t// REF: https://fly.io/docs/machines/api/certificates-resource/#import-custom-certificate\n\timportCustomCertificateReq := &flyiosdk.ImportCustomCertificateRequest{\n\t\tAppName:    d.config.AppName,\n\t\tHostname:   d.config.Domain,\n\t\tFullchain:  certPEM,\n\t\tPrivateKey: privkeyPEM,\n\t}\n\timportCustomCertificateResp, err := d.sdkClient.ImportCustomCertificateWithContext(ctx, importCustomCertificateReq)\n\td.logger.Debug(\"sdk request 'flyio.ImportCustomCertificate'\", slog.Any(\"request\", importCustomCertificateReq), slog.Any(\"response\", importCustomCertificateResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'flyio.ImportCustomCertificate': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(apiToken string) (*flyiosdk.Client, error) {\n\treturn flyiosdk.NewClient(apiToken)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/flyio/flyio_test.go",
    "content": "package flyio_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/flyio\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfApiToken      string\n\tfAppName       string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"FLYIO_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fApiToken, argsPrefix+\"APITOKEN\", \"\", \"\")\n\tflag.StringVar(&fAppName, argsPrefix+\"APPNAME\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./flyio_test.go -args \\\n\t--FLYIO_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--FLYIO_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--FLYIO_APITOKEN=\"your-api-token\" \\\n\t--FLYIO_APPNAME=\"your-app-name\" \\\n\t--FLYIO_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"APITOKEN: %v\", fApiToken),\n\t\t\tfmt.Sprintf(\"APPNAME: %v\", fAppName),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tApiToken: fApiToken,\n\t\t\tAppName:  fAppName,\n\t\t\tDomain:   fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go",
    "content": "package gcorecdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\n\t\"github.com/G-Core/gcorelabscdn-go/gcore\"\n\t\"github.com/G-Core/gcorelabscdn-go/gcore/provider\"\n\t\"github.com/G-Core/gcorelabscdn-go/resources\"\n\t\"github.com/G-Core/gcorelabscdn-go/sslcerts\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/gcore-cdn\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tgcoresdk \"github.com/certimate-go/certimate/pkg/sdk3rd/gcore\"\n)\n\ntype DeployerConfig struct {\n\t// G-Core API Token。\n\tApiToken string `json:\"apiToken\"`\n\t// CDN 资源 ID。\n\tResourceId int64 `json:\"resourceId\"`\n\t// 证书 ID。\n\t// 选填。零值时表示新建证书；否则表示更新证书。\n\tCertificateId int64 `json:\"certificateId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClients *wSDKClients\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\ntype wSDKClients struct {\n\tResources *resources.Service\n\tSSLCerts  *sslcerts.Service\n}\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclients, err := createSDKClients(config.ApiToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tApiToken: config.ApiToken,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClients: clients,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.ResourceId == 0 {\n\t\treturn nil, errors.New(\"config `resourceId` is required\")\n\t}\n\n\t// 如果原证书 ID 为空，则创建证书；否则更新证书。\n\tvar cloudCertId int64\n\tif d.config.CertificateId == 0 {\n\t\t// 上传证书\n\t\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t\t} else {\n\t\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t\t}\n\n\t\tcloudCertId, _ = strconv.ParseInt(upres.CertId, 10, 64)\n\t} else {\n\t\t// 获取证书\n\t\t// REF: https://api.gcore.com/docs/cdn#tag/SSL-certificates/paths/~1cdn~1sslData~1%7Bssl_id%7D/get\n\t\tgetCertificateDetailResp, err := d.sdkClients.SSLCerts.Get(ctx, d.config.CertificateId)\n\t\td.logger.Debug(\"sdk request 'sslcerts.Get'\", slog.Int64(\"sslId\", d.config.CertificateId), slog.Any(\"response\", getCertificateDetailResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'sslcerts.Get': %w\", err)\n\t\t}\n\n\t\t// 更新证书\n\t\t// REF: https://api.gcore.com/docs/cdn#tag/SSL-certificates/paths/~1cdn~1sslData~1%7Bssl_id%7D/get\n\t\tchangeCertificateReq := &sslcerts.UpdateRequest{\n\t\t\tName:           getCertificateDetailResp.Name,\n\t\t\tCert:           certPEM,\n\t\t\tPrivateKey:     privkeyPEM,\n\t\t\tValidateRootCA: false,\n\t\t}\n\t\tchangeCertificateResp, err := d.sdkClients.SSLCerts.Update(ctx, getCertificateDetailResp.ID, changeCertificateReq)\n\t\td.logger.Debug(\"sdk request 'sslcerts.Update'\", slog.Int64(\"sslId\", getCertificateDetailResp.ID), slog.Any(\"request\", changeCertificateReq), slog.Any(\"response\", changeCertificateResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'sslcerts.Update': %w\", err)\n\t\t}\n\n\t\tcloudCertId = changeCertificateResp.ID\n\t}\n\n\t// 获取 CDN 资源详情\n\t// REF: https://api.gcore.com/docs/cdn#tag/CDN-resources/paths/~1cdn~1resources~1%7Bresource_id%7D/get\n\tgetResourceResp, err := d.sdkClients.Resources.Get(ctx, d.config.ResourceId)\n\td.logger.Debug(\"sdk request 'resources.Get'\", slog.Any(\"resourceId\", d.config.ResourceId), slog.Any(\"response\", getResourceResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'resources.Get': %w\", err)\n\t}\n\n\t// 更新 CDN 资源详情\n\t// REF: https://api.gcore.com/docs/cdn#tag/CDN-resources/operation/change_cdn_resource\n\tupdateResourceReq := &resources.UpdateRequest{\n\t\tDescription:        getResourceResp.Description,\n\t\tActive:             getResourceResp.Active,\n\t\tOriginGroup:        int(getResourceResp.OriginGroup),\n\t\tOriginProtocol:     getResourceResp.OriginProtocol,\n\t\tSecondaryHostnames: getResourceResp.SecondaryHostnames,\n\t\tSSlEnabled:         true,\n\t\tSSLData:            int(cloudCertId),\n\t\tProxySSLEnabled:    getResourceResp.ProxySSLEnabled,\n\t\tOptions:            &gcore.Options{},\n\t}\n\tif getResourceResp.ProxySSLCA != 0 {\n\t\tupdateResourceReq.ProxySSLCA = &getResourceResp.ProxySSLCA\n\t}\n\tif getResourceResp.ProxySSLData != 0 {\n\t\tupdateResourceReq.ProxySSLData = &getResourceResp.ProxySSLData\n\t}\n\tupdateResourceResp, err := d.sdkClients.Resources.Update(ctx, d.config.ResourceId, updateResourceReq)\n\td.logger.Debug(\"sdk request 'resources.Update'\", slog.Int64(\"resourceId\", d.config.ResourceId), slog.Any(\"request\", updateResourceReq), slog.Any(\"response\", updateResourceResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'resources.Update': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClients(apiToken string) (*wSDKClients, error) {\n\tif apiToken == \"\" {\n\t\treturn nil, errors.New(\"gcore: invalid api token\")\n\t}\n\n\trequester := provider.NewClient(\n\t\tgcoresdk.BASE_URL,\n\t\tprovider.WithSigner(gcoresdk.NewAuthRequestSigner(apiToken)),\n\t)\n\tresourcesSrv := resources.NewService(requester)\n\tsslCertsSrv := sslcerts.NewService(requester)\n\treturn &wSDKClients{\n\t\tResources: resourcesSrv,\n\t\tSSLCerts:  sslCertsSrv,\n\t}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/gcore-cdn/gcore_cdn_test.go",
    "content": "package gcorecdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/gcore-cdn\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfApiToken      string\n\tfResourceId    int64\n)\n\nfunc init() {\n\targsPrefix := \"GCORECDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fApiToken, argsPrefix+\"APITOKEN\", \"\", \"\")\n\tflag.Int64Var(&fResourceId, argsPrefix+\"RESOURCEID\", 0, \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./gcore_cdn_test.go -args \\\n\t--GCORECDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--GCORECDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--GCORECDN_APITOKEN=\"your-api-token\" \\\n\t--GCORECDN_RESOURCEID=\"your-cdn-resource-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"APITOKEN: %v\", fApiToken),\n\t\t\tfmt.Sprintf(\"RESOURCEID: %v\", fResourceId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tApiToken:   fApiToken,\n\t\t\tResourceId: fResourceId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/goedge/consts.go",
    "content": "package goedge\n\nconst (\n\t// 资源类型：替换指定证书。\n\tRESOURCE_TYPE_CERTIFICATE = \"certificate\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/goedge/goedge.go",
    "content": "package goedge\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tgoedgesdk \"github.com/certimate-go/certimate/pkg/sdk3rd/goedge\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype DeployerConfig struct {\n\t// GoEdge 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// GoEdge 用户角色。\n\t// 可取值 \"user\"、\"admin\"。\n\tApiRole string `json:\"apiRole\"`\n\t// GoEdge AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// GoEdge AccessKey。\n\tAccessKey string `json:\"accessKey\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 证书 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。\n\tCertificateId int64 `json:\"certificateId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *goedgesdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiRole, config.AccessKeyId, config.AccessKey, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_CERTIFICATE:\n\t\tif err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.CertificateId == 0 {\n\t\treturn errors.New(\"config `certificateId` is required\")\n\t}\n\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 修改证书\n\t// REF: https://goedge.cloud/dev/api/service/SSLCertService?role=user#updateSSLCert\n\tupdateSSLCertReq := &goedgesdk.UpdateSSLCertRequest{\n\t\tSSLCertId:   d.config.CertificateId,\n\t\tIsOn:        true,\n\t\tName:        fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli()),\n\t\tDescription: \"upload from certimate\",\n\t\tServerName:  certX509.Subject.CommonName,\n\t\tIsCA:        false,\n\t\tCertData:    base64.StdEncoding.EncodeToString([]byte(certPEM)),\n\t\tKeyData:     base64.StdEncoding.EncodeToString([]byte(privkeyPEM)),\n\t\tTimeBeginAt: certX509.NotBefore.Unix(),\n\t\tTimeEndAt:   certX509.NotAfter.Unix(),\n\t\tDNSNames:    certX509.DNSNames,\n\t\tCommonNames: []string{certX509.Subject.CommonName},\n\t}\n\tupdateSSLCertResp, err := d.sdkClient.UpdateSSLCertWithContext(ctx, updateSSLCertReq)\n\td.logger.Debug(\"sdk request 'goedge.UpdateSSLCert'\", slog.Any(\"request\", updateSSLCertReq), slog.Any(\"response\", updateSSLCertResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'goedge.UpdateSSLCert': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(serverUrl, apiRole, accessKeyId, accessKey string, skipTlsVerify bool) (*goedgesdk.Client, error) {\n\tclient, err := goedgesdk.NewClient(serverUrl, apiRole, accessKeyId, accessKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/goedge/goedge_test.go",
    "content": "package goedge_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/goedge\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfAccessKeyId   string\n\tfAccessKey     string\n\tfCertificateId int64\n)\n\nfunc init() {\n\targsPrefix := \"GOEDGE_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKey, argsPrefix+\"ACCESSKEY\", \"\", \"\")\n\tflag.Int64Var(&fCertificateId, argsPrefix+\"CERTIFICATEID\", 0, \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./goedge_test.go -args \\\n\t--GOEDGE_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--GOEDGE_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--GOEDGE_SERVERURL=\"http://127.0.0.1:7788\" \\\n\t--GOEDGE_ACCESSKEYID=\"your-access-key-id\" \\\n\t--GOEDGE_ACCESSKEY=\"your-access-key\" \\\n\t--GOEDGE_CERTIFICATEID=\"your-certificate-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy_ToCertificate\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEY: %v\", fAccessKey),\n\t\t\tfmt.Sprintf(\"CERTIFICATEID: %v\", fCertificateId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiRole:                  \"user\",\n\t\t\tAccessKeyId:              fAccessKeyId,\n\t\t\tAccessKey:                fAccessKey,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tResourceType:             provider.RESOURCE_TYPE_CERTIFICATE,\n\t\t\tCertificateId:            fCertificateId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/huaweicloud-cdn/consts.go",
    "content": "package huaweicloudcdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go",
    "content": "package huaweicloudcdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global\"\n\thccdn \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2\"\n\thccdnmodel \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model\"\n\thccdnregion \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/region\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/huaweicloud-scm\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-cdn/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 华为云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 华为云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 华为云企业项目 ID。\n\tEnterpriseProjectId string `json:\"enterpriseProjectId,omitempty\"`\n\t// 华为云区域。\n\tRegion string `json:\"region\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.CdnClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(\n\t\tconfig.AccessKeyId,\n\t\tconfig.SecretAccessKey,\n\t\tconfig.Region,\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:         config.AccessKeyId,\n\t\tSecretAccessKey:     config.SecretAccessKey,\n\t\tEnterpriseProjectId: config.EnterpriseProjectId,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain)\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no cdn domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found cdn domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tconst MAX_DOMAIN_PER_REQUEST = 50\n\t\tdomainChunks := lo.Chunk(domains, MAX_DOMAIN_PER_REQUEST)\n\t\tfor _, domains := range domainChunks {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainsCertificate(ctx, domains, upres.CertId, upres.CertName); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// REF: https://support.huaweicloud.com/api-cdn/ListDomains.html\n\tlistDomainsPageNumber := 1\n\tlistDomainsPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistDomainsReq := &hccdnmodel.ListDomainsRequest{\n\t\t\tEnterpriseProjectId: lo.EmptyableToPtr(d.config.EnterpriseProjectId),\n\t\t\tPageNumber:          lo.ToPtr(int32(listDomainsPageNumber)),\n\t\t\tPageSize:            lo.ToPtr(int32(listDomainsPageSize)),\n\t\t}\n\t\tlistDomainsResp, err := d.sdkClient.ListDomains(listDomainsReq)\n\t\td.logger.Debug(\"sdk request 'cdn.ListDomains'\", slog.Any(\"request\", listDomainsReq), slog.Any(\"response\", listDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.ListDomains': %w\", err)\n\t\t}\n\n\t\tif listDomainsResp.Domains == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tignoredStatuses := []string{\"offline\", \"checking\", \"check_failed\", \"deleting\"}\n\t\tfor _, domainItem := range *listDomainsResp.Domains {\n\t\t\tif lo.Contains(ignoredStatuses, lo.FromPtr(domainItem.DomainStatus)) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdomains = append(domains, lo.FromPtr(domainItem.DomainName))\n\t\t}\n\n\t\tif len(*listDomainsResp.Domains) < listDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tlistDomainsPageNumber++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainsCertificate(ctx context.Context, domains []string, cloudCertId, cloudCertName string) error {\n\t// 更新加速域名配置\n\t// REF: https://support.huaweicloud.com/api-cdn/UpdateDomainMultiCertificates.html\n\t// REF: https://support.huaweicloud.com/usermanual-cdn/cdn_01_0306.html\n\tupdateDomainMultiCertificatesReq := &hccdnmodel.UpdateDomainMultiCertificatesRequest{\n\t\tEnterpriseProjectId: lo.EmptyableToPtr(d.config.EnterpriseProjectId),\n\t\tBody: &hccdnmodel.UpdateDomainMultiCertificatesRequestBody{\n\t\t\tHttps: &hccdnmodel.UpdateDomainMultiCertificatesRequestBodyContent{\n\t\t\t\tDomainName:       strings.Join(domains, \",\"),\n\t\t\t\tHttpsSwitch:      1,\n\t\t\t\tCertificateType:  lo.ToPtr(int32(2)),\n\t\t\t\tScmCertificateId: lo.ToPtr(cloudCertId),\n\t\t\t\tCertName:         lo.ToPtr(cloudCertName),\n\t\t\t},\n\t\t},\n\t}\n\tupdateDomainMultiCertificatesResp, err := d.sdkClient.UpdateDomainMultiCertificates(updateDomainMultiCertificatesReq)\n\td.logger.Debug(\"sdk request 'cdn.UpdateDomainMultiCertificates'\", slog.Any(\"request\", updateDomainMultiCertificatesReq), slog.Any(\"response\", updateDomainMultiCertificatesResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.UpdateDomainMultiCertificates': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey, region string) (*internal.CdnClient, error) {\n\tif region == \"\" {\n\t\tregion = \"cn-north-1\" // CDN 服务默认区域：华北一北京\n\t}\n\n\tauth, err := global.NewCredentialsBuilder().\n\t\tWithAk(accessKeyId).\n\t\tWithSk(secretAccessKey).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thcRegion, err := hccdnregion.SafeValueOf(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thcClient, err := hccdn.CdnClientBuilder().\n\t\tWithRegion(hcRegion).\n\t\tWithCredential(auth).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := internal.NewCdnClient(hcClient)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn_test.go",
    "content": "package huaweicloudcdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-cdn\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n\tfRegion          string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"HUAWEICLOUDCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./huaweicloud_cdn_test.go -args \\\n\t--HUAWEICLOUDCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--HUAWEICLOUDCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--HUAWEICLOUDCDN_ACCESSKEYID=\"your-access-key-id\" \\\n\t--HUAWEICLOUDCDN_SECRETACCESSKEY=\"your-secret-access-key\" \\\n\t--HUAWEICLOUDCDN_REGION=\"cn-north-1\" \\\n\t--HUAWEICLOUDCDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tSecretAccessKey:    fSecretAccessKey,\n\t\t\tRegion:             fRegion,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/huaweicloud-cdn/internal/client.go",
    "content": "package internal\n\nimport (\n\thttpclient \"github.com/huaweicloud/huaweicloud-sdk-go-v3/core\"\n\thwcdn \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2\"\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model\"\n)\n\n// This is a partial copy of https://github.com/huaweicloud/huaweicloud-sdk-go-v3/blob/master/services/cdn/v2/cdn_client.go\n// to lightweight the vendor packages in the built binary.\ntype CdnClient struct {\n\tHcClient *httpclient.HcHttpClient\n}\n\nfunc NewCdnClient(hcClient *httpclient.HcHttpClient) *CdnClient {\n\treturn &CdnClient{HcClient: hcClient}\n}\n\nfunc (c *CdnClient) ListDomains(request *model.ListDomainsRequest) (*model.ListDomainsResponse, error) {\n\trequestDef := hwcdn.GenReqDefForListDomains()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ListDomainsResponse), nil\n\t}\n}\n\nfunc (c *CdnClient) UpdateDomainMultiCertificates(request *model.UpdateDomainMultiCertificatesRequest) (*model.UpdateDomainMultiCertificatesResponse, error) {\n\trequestDef := hwcdn.GenReqDefForUpdateDomainMultiCertificates()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.UpdateDomainMultiCertificatesResponse), nil\n\t}\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/huaweicloud-elb/consts.go",
    "content": "package huaweicloudelb\n\nconst (\n\t// 资源类型：替换指定证书。\n\tRESOURCE_TYPE_CERTIFICATE = \"certificate\"\n\t// 资源类型：部署到指定负载均衡器。\n\tRESOURCE_TYPE_LOADBALANCER = \"loadbalancer\"\n\t// 资源类型：部署到指定监听器。\n\tRESOURCE_TYPE_LISTENER = \"listener\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go",
    "content": "package huaweicloudelb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic\"\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global\"\n\thcelb \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3\"\n\thcelbmodel \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model\"\n\thcelbregion \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/region\"\n\thciam \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3\"\n\thciammodel \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model\"\n\thciamregion \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/huaweicloud-elb\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-elb/internal\"\n)\n\ntype DeployerConfig struct {\n\t// 华为云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 华为云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 华为云企业项目 ID。\n\tEnterpriseProjectId string `json:\"enterpriseProjectId,omitempty\"`\n\t// 华为云区域。\n\tRegion string `json:\"region\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 证书 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。\n\tCertificateId string `json:\"certificateId,omitempty\"`\n\t// 负载均衡器 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。\n\tLoadbalancerId string `json:\"loadbalancerId,omitempty\"`\n\t// 负载均衡监听 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。\n\tListenerId string `json:\"listenerId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.ElbClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:         config.AccessKeyId,\n\t\tSecretAccessKey:     config.SecretAccessKey,\n\t\tEnterpriseProjectId: config.EnterpriseProjectId,\n\t\tRegion:              config.Region,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_CERTIFICATE:\n\t\tif err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_LOADBALANCER:\n\t\tif err := d.deployToLoadbalancer(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_LISTENER:\n\t\tif err := d.deployToListener(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToLoadbalancer(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\n\t// 查询负载均衡器详情\n\t// REF: https://support.huaweicloud.com/api-elb/ShowLoadBalancer.html\n\tshowLoadBalancerReq := &hcelbmodel.ShowLoadBalancerRequest{\n\t\tLoadbalancerId: d.config.LoadbalancerId,\n\t}\n\tshowLoadBalancerResp, err := d.sdkClient.ShowLoadBalancer(showLoadBalancerReq)\n\td.logger.Debug(\"sdk request 'elb.ShowLoadBalancer'\", slog.Any(\"request\", showLoadBalancerReq), slog.Any(\"response\", showLoadBalancerResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'elb.ShowLoadBalancer': %w\", err)\n\t}\n\n\t// 查询监听器列表\n\t// REF: https://support.huaweicloud.com/api-elb/ListListeners.html\n\tlistenerIds := make([]string, 0)\n\tlistListenersMarker := (*string)(nil)\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistListenersReq := &hcelbmodel.ListListenersRequest{\n\t\t\tMarker:         listListenersMarker,\n\t\t\tLimit:          lo.ToPtr(int32(2000)),\n\t\t\tProtocol:       &[]string{\"HTTPS\", \"TERMINATED_HTTPS\"},\n\t\t\tLoadbalancerId: &[]string{showLoadBalancerResp.Loadbalancer.Id},\n\t\t}\n\t\tif d.config.EnterpriseProjectId != \"\" {\n\t\t\tlistListenersReq.EnterpriseProjectId = lo.ToPtr([]string{d.config.EnterpriseProjectId})\n\t\t}\n\t\tlistListenersResp, err := d.sdkClient.ListListeners(listListenersReq)\n\t\td.logger.Debug(\"sdk request 'elb.ListListeners'\", slog.Any(\"request\", listListenersReq), slog.Any(\"response\", listListenersResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'elb.ListListeners': %w\", err)\n\t\t}\n\n\t\tif listListenersResp.Listeners == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, listener := range *listListenersResp.Listeners {\n\t\t\tlistenerIds = append(listenerIds, listener.Id)\n\t\t}\n\n\t\tif len(*listListenersResp.Listeners) == 0 || listListenersResp.PageInfo.NextMarker == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tlistListenersMarker = listListenersResp.PageInfo.NextMarker\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 遍历更新监听器证书\n\tif len(listenerIds) == 0 {\n\t\td.logger.Info(\"no listeners to deploy\")\n\t} else {\n\t\td.logger.Info(\"found https listeners to deploy\", slog.Any(\"listenerIds\", listenerIds))\n\t\tvar errs []error\n\n\t\tfor _, listenerId := range listenerIds {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateListenerCertificate(ctx, listenerId, upres.CertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToListener(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.ListenerId == \"\" {\n\t\treturn errors.New(\"config `listenerId` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 更新监听器证书\n\tif err := d.updateListenerCertificate(ctx, d.config.ListenerId, upres.CertId); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.CertificateId == \"\" {\n\t\treturn errors.New(\"config `certificateId` is required\")\n\t}\n\n\t// 替换证书\n\topres, err := d.sdkCertmgr.Replace(ctx, d.config.CertificateId, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to replace certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate replaced\", slog.Any(\"result\", opres))\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {\n\t// 查询监听器详情\n\t// REF: https://support.huaweicloud.com/api-elb/ShowListener.html\n\tshowListenerReq := &hcelbmodel.ShowListenerRequest{\n\t\tListenerId: cloudListenerId,\n\t}\n\tshowListenerResp, err := d.sdkClient.ShowListener(showListenerReq)\n\td.logger.Debug(\"sdk request 'elb.ShowListener'\", slog.Any(\"request\", showListenerReq), slog.Any(\"response\", showListenerResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'elb.ShowListener': %w\", err)\n\t}\n\n\t// 更新监听器\n\t// REF: https://support.huaweicloud.com/api-elb/UpdateListener.html\n\tupdateListenerReq := &hcelbmodel.UpdateListenerRequest{\n\t\tListenerId: cloudListenerId,\n\t\tBody: &hcelbmodel.UpdateListenerRequestBody{\n\t\t\tListener: &hcelbmodel.UpdateListenerOption{\n\t\t\t\tDefaultTlsContainerRef: lo.ToPtr(cloudCertId),\n\t\t\t},\n\t\t},\n\t}\n\tif showListenerResp.Listener.SniContainerRefs != nil {\n\t\tif len(showListenerResp.Listener.SniContainerRefs) > 0 {\n\t\t\t// 如果开启 SNI，需替换同 SAN 的证书\n\t\t\tsniCertIds := make([]string, 0)\n\t\t\tsniCertIds = append(sniCertIds, cloudCertId)\n\n\t\t\tlistOldCertificateReq := &hcelbmodel.ListCertificatesRequest{\n\t\t\t\tId: &showListenerResp.Listener.SniContainerRefs,\n\t\t\t}\n\t\t\tlistOldCertificateResp, err := d.sdkClient.ListCertificates(listOldCertificateReq)\n\t\t\td.logger.Debug(\"sdk request 'elb.ListCertificates'\", slog.Any(\"request\", listOldCertificateReq), slog.Any(\"response\", listOldCertificateResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'elb.ListCertificates': %w\", err)\n\t\t\t}\n\n\t\t\tshowNewCertificateReq := &hcelbmodel.ShowCertificateRequest{\n\t\t\t\tCertificateId: cloudCertId,\n\t\t\t}\n\t\t\tshowNewCertificateResp, err := d.sdkClient.ShowCertificate(showNewCertificateReq)\n\t\t\td.logger.Debug(\"sdk request 'elb.ShowCertificate'\", slog.Any(\"request\", showNewCertificateReq), slog.Any(\"response\", showNewCertificateResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'elb.ShowCertificate': %w\", err)\n\t\t\t}\n\n\t\t\tfor _, oldCertInfo := range *listOldCertificateResp.Certificates {\n\t\t\t\tnewCertInfo := showNewCertificateResp.Certificate\n\n\t\t\t\tif oldCertInfo.SubjectAlternativeNames != nil && newCertInfo.SubjectAlternativeNames != nil {\n\t\t\t\t\tif strings.Join(*oldCertInfo.SubjectAlternativeNames, \",\") == strings.Join(*newCertInfo.SubjectAlternativeNames, \",\") {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif oldCertInfo.Domain == newCertInfo.Domain {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsniCertIds = append(sniCertIds, oldCertInfo.Id)\n\t\t\t}\n\n\t\t\tupdateListenerReq.Body.Listener.SniContainerRefs = &sniCertIds\n\t\t}\n\n\t\tif showListenerResp.Listener.SniMatchAlgo != \"\" {\n\t\t\tupdateListenerReq.Body.Listener.SniMatchAlgo = lo.ToPtr(showListenerResp.Listener.SniMatchAlgo)\n\t\t}\n\t}\n\tupdateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq)\n\td.logger.Debug(\"sdk request 'elb.UpdateListener'\", slog.Any(\"request\", updateListenerReq), slog.Any(\"response\", updateListenerResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'elb.UpdateListener': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey, region string) (*internal.ElbClient, error) {\n\tprojectId, err := getSDKProjectId(accessKeyId, secretAccessKey, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tauth, err := basic.NewCredentialsBuilder().\n\t\tWithAk(accessKeyId).\n\t\tWithSk(secretAccessKey).\n\t\tWithProjectId(projectId).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thcRegion, err := hcelbregion.SafeValueOf(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thcClient, err := hcelb.ElbClientBuilder().\n\t\tWithRegion(hcRegion).\n\t\tWithCredential(auth).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := internal.NewElbClient(hcClient)\n\treturn client, nil\n}\n\nfunc getSDKProjectId(accessKeyId, secretAccessKey, region string) (string, error) {\n\tif region == \"\" {\n\t\tregion = \"cn-north-4\" // IAM 服务默认区域：华北四北京\n\t}\n\n\tauth, err := global.NewCredentialsBuilder().\n\t\tWithAk(accessKeyId).\n\t\tWithSk(secretAccessKey).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thcRegion, err := hciamregion.SafeValueOf(region)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thcClient, err := hciam.IamClientBuilder().\n\t\tWithRegion(hcRegion).\n\t\tWithCredential(auth).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tclient := hciam.NewIamClient(hcClient)\n\n\trequest := &hciammodel.KeystoneListProjectsRequest{\n\t\tName: &region,\n\t}\n\tresponse, err := client.KeystoneListProjects(request)\n\tif err != nil {\n\t\treturn \"\", err\n\t} else if response.Projects == nil || len(*response.Projects) == 0 {\n\t\treturn \"\", errors.New(\"huaweicloud: no project found\")\n\t}\n\n\treturn (*response.Projects)[0].Id, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb_test.go",
    "content": "package huaweicloudelb_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-elb\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n\tfRegion          string\n\tfCertificateId   string\n\tfLoadbalancerId  string\n\tfListenerId      string\n)\n\nfunc init() {\n\targsPrefix := \"HUAWEICLOUDELB_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fCertificateId, argsPrefix+\"CERTIFICATEID\", \"\", \"\")\n\tflag.StringVar(&fLoadbalancerId, argsPrefix+\"LOADBALANCERID\", \"\", \"\")\n\tflag.StringVar(&fListenerId, argsPrefix+\"LISTENERID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./huaweicloud_elb_test.go -args \\\n\t--HUAWEICLOUDELB_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--HUAWEICLOUDELB_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--HUAWEICLOUDELB_ACCESSKEYID=\"your-access-key-id\" \\\n\t--HUAWEICLOUDELB_SECRETACCESSKEY=\"your-secret-access-key\" \\\n\t--HUAWEICLOUDELB_REGION=\"cn-north-1\" \\\n\t--HUAWEICLOUDELB_CERTIFICATEID=\"your-elb-cert-id\" \\\n\t--HUAWEICLOUDELB_LOADBALANCERID=\"your-elb-loadbalancer-id\" \\\n\t--HUAWEICLOUDELB_LISTENERID=\"your-elb-listener-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy_ToCertificate\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"CERTIFICATEID: %v\", fCertificateId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t\tRegion:          fRegion,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_CERTIFICATE,\n\t\t\tCertificateId:   fCertificateId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n\n\tt.Run(\"Deploy_ToLoadbalancer\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LOADBALANCERID: %v\", fLoadbalancerId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t\tRegion:          fRegion,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LOADBALANCER,\n\t\t\tLoadbalancerId:  fLoadbalancerId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n\n\tt.Run(\"Deploy_ToListenerId\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LISTENERID: %v\", fListenerId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t\tRegion:          fRegion,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LISTENER,\n\t\t\tListenerId:      fListenerId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/huaweicloud-elb/internal/client.go",
    "content": "package internal\n\nimport (\n\thttpclient \"github.com/huaweicloud/huaweicloud-sdk-go-v3/core\"\n\thwelb \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3\"\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model\"\n)\n\n// This is a partial copy of https://github.com/huaweicloud/huaweicloud-sdk-go-v3/blob/master/services/elb/v3/elb_client.go\n// to lightweight the vendor packages in the built binary.\ntype ElbClient struct {\n\tHcClient *httpclient.HcHttpClient\n}\n\nfunc NewElbClient(hcClient *httpclient.HcHttpClient) *ElbClient {\n\treturn &ElbClient{HcClient: hcClient}\n}\n\nfunc (c *ElbClient) ListCertificates(request *model.ListCertificatesRequest) (*model.ListCertificatesResponse, error) {\n\trequestDef := hwelb.GenReqDefForListCertificates()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ListCertificatesResponse), nil\n\t}\n}\n\nfunc (c *ElbClient) ListListeners(request *model.ListListenersRequest) (*model.ListListenersResponse, error) {\n\trequestDef := hwelb.GenReqDefForListListeners()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ListListenersResponse), nil\n\t}\n}\n\nfunc (c *ElbClient) ShowCertificate(request *model.ShowCertificateRequest) (*model.ShowCertificateResponse, error) {\n\trequestDef := hwelb.GenReqDefForShowCertificate()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ShowCertificateResponse), nil\n\t}\n}\n\nfunc (c *ElbClient) ShowListener(request *model.ShowListenerRequest) (*model.ShowListenerResponse, error) {\n\trequestDef := hwelb.GenReqDefForShowListener()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ShowListenerResponse), nil\n\t}\n}\n\nfunc (c *ElbClient) ShowLoadBalancer(request *model.ShowLoadBalancerRequest) (*model.ShowLoadBalancerResponse, error) {\n\trequestDef := hwelb.GenReqDefForShowLoadBalancer()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ShowLoadBalancerResponse), nil\n\t}\n}\n\nfunc (c *ElbClient) UpdateListener(request *model.UpdateListenerRequest) (*model.UpdateListenerResponse, error) {\n\trequestDef := hwelb.GenReqDefForUpdateListener()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.UpdateListenerResponse), nil\n\t}\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/huaweicloud-obs/huaweicloud_obs.go",
    "content": "package huaweicloudobs\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/md5\"\n\t\"crypto/sha1\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// 华为云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 华为云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 华为云区域。\n\tRegion string `json:\"region\"`\n\t// 存储桶名。\n\tBucket string `json:\"bucket\"`\n\t// 自定义域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig *DeployerConfig\n\tlogger *slog.Logger\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\treturn &Deployer{\n\t\tconfig: config,\n\t\tlogger: slog.Default(),\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.Region == \"\" {\n\t\treturn nil, fmt.Errorf(\"config `region` is required\")\n\t}\n\tif d.config.Bucket == \"\" {\n\t\treturn nil, fmt.Errorf(\"config `bucket` is required\")\n\t}\n\tif d.config.Domain == \"\" {\n\t\treturn nil, fmt.Errorf(\"config `domain` is required\")\n\t}\n\n\t// REF: https://support.huaweicloud.com/usermanual-obs/obs_06_3200.html\n\t// REF: https://support.huaweicloud.com/api-obs/obs_04_0059.html\n\turl := fmt.Sprintf(\"https://%s.obs.%s.myhuaweicloud.com/?customdomain=%s\", d.config.Bucket, d.config.Region, d.config.Domain)\n\tbodyXML := fmt.Sprintf(`\n<CustomDomainConfiguration>\n\t<Name>%s</Name>\n\t<Certificate>%s</Certificate>\n\t<CertificateChain>%s</CertificateChain>\n\t<PrivateKey>%s</PrivateKey>\n</CustomDomainConfiguration>`,\n\t\td.config.Bucket+\"_\"+d.config.Domain, certPEM, certPEM, privkeyPEM,\n\t)\n\n\t// 计算 Content-MD5（Base64 编码）\n\tmd5sum := md5.Sum([]byte(bodyXML))\n\tmd5sumEncoded := base64.StdEncoding.EncodeToString(md5sum[:])\n\n\t// 构造签名字符串\n\tdate := time.Now().UTC().Format(http.TimeFormat)\n\tmethod := \"PUT\"\n\tcontentType := \"application/xml\"\n\tcanonicalizedResource := fmt.Sprintf(\"/%s/?customdomain=%s\", d.config.Bucket, d.config.Domain)\n\tstringToSign := fmt.Sprintf(\"%s\\n%s\\n%s\\n%s\\n%s\", method, md5sumEncoded, contentType, date, canonicalizedResource)\n\n\t// HMAC-SHA1 签名\n\th := hmac.New(sha1.New, []byte(d.config.SecretAccessKey))\n\th.Write([]byte(stringToSign))\n\tsignature := base64.StdEncoding.EncodeToString(h.Sum(nil))\n\n\t// Authorization\n\tauthHeader := fmt.Sprintf(\"OBS %s:%s\", d.config.AccessKeyId, signature)\n\n\t// 创建请求\n\treq, err := http.NewRequest(method, url, bytes.NewBuffer([]byte(bodyXML)))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"huaweicloud obs api error: %w\", err)\n\t}\n\treq.Header.Set(\"Date\", date)\n\treq.Header.Set(\"Authorization\", authHeader)\n\treq.Header.Set(\"Content-MD5\", md5sumEncoded)\n\treq.Header.Set(\"Content-Type\", contentType)\n\n\t// 请求\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"huaweicloud obs api error: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\t// 响应\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody := &bytes.Buffer{}\n\t\tbody.ReadFrom(resp.Body)\n\t\treturn nil, fmt.Errorf(\"huaweicloud obs api error: unexpected status code: %d (resp: %s)\", resp.StatusCode, body.String())\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/huaweicloud-obs/huaweicloud_obs_test.go",
    "content": "package huaweicloudobs_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-obs\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n\tfRegion          string\n\tfBucket          string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"HUAWEICLOUDOBS_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fBucket, argsPrefix+\"BUCKET\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./huaweicloud_obs_test.go -args \\\n\t--HUAWEICLOUDOBS_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--HUAWEICLOUDOBS_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--HUAWEICLOUDOBS_ACCESSKEYID=\"your-access-key-id\" \\\n\t--HUAWEICLOUDOBS_SECRETACCESSKEY=\"your-secret-access-key\" \\\n\t--HUAWEICLOUDOBS_REGION=\"cn-north-4\" \\\n\t--HUAWEICLOUDOBS_BUCKET=\"your-bucket\" \\\n\t--HUAWEICLOUDOBS_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"BUCKET: %v\", fBucket),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t\tRegion:          fRegion,\n\t\t\tBucket:          fBucket,\n\t\t\tDomain:          fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go",
    "content": "package huaweicloudscm\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/huaweicloud-scm\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// 华为云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 华为云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 华为云企业项目 ID。\n\tEnterpriseProjectId string `json:\"enterpriseProjectId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:         config.AccessKeyId,\n\t\tSecretAccessKey:     config.SecretAccessKey,\n\t\tEnterpriseProjectId: config.EnterpriseProjectId,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/huaweicloud-waf/consts.go",
    "content": "package huaweicloudwaf\n\nconst (\n\t// 资源类型：部署到云模式防护网站。\n\tRESOURCE_TYPE_CLOUDSERVER = \"cloudserver\"\n\t// 资源类型：部署到独享模式防护网站。\n\tRESOURCE_TYPE_PREMIUMHOST = \"premiumhost\"\n\t// 资源类型：替换指定证书。\n\tRESOURCE_TYPE_CERTIFICATE = \"certificate\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go",
    "content": "package huaweicloudwaf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic\"\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global\"\n\thciam \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3\"\n\thciamModel \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model\"\n\thciamregion \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region\"\n\thcwaf \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1\"\n\thcwafmodel \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/model\"\n\thcwafregion \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/region\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/huaweicloud-waf\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-waf/internal\"\n)\n\ntype DeployerConfig struct {\n\t// 华为云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 华为云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 华为云企业项目 ID。\n\tEnterpriseProjectId string `json:\"enterpriseProjectId,omitempty\"`\n\t// 华为云区域。\n\tRegion string `json:\"region\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 证书 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。\n\tCertificateId string `json:\"certificateId,omitempty\"`\n\t// 防护域名（支持泛域名）。\n\t// 部署资源类型为 [RESOURCE_TYPE_CLOUDSERVER]、[RESOURCE_TYPE_PREMIUMHOST] 时必填。\n\tDomain string `json:\"domain,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.WafClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:         config.AccessKeyId,\n\t\tSecretAccessKey:     config.SecretAccessKey,\n\t\tEnterpriseProjectId: config.EnterpriseProjectId,\n\t\tRegion:              config.Region,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_CLOUDSERVER:\n\t\tif err := d.deployToCloudServer(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_PREMIUMHOST:\n\t\tif err := d.deployToPremiumHost(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_CERTIFICATE:\n\t\tif err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.CertificateId == \"\" {\n\t\treturn errors.New(\"config `certificateId` is required\")\n\t}\n\n\t// 查询证书\n\t// REF: https://support.huaweicloud.com/api-waf/ShowCertificate.html\n\tshowCertificateReq := &hcwafmodel.ShowCertificateRequest{\n\t\tEnterpriseProjectId: lo.EmptyableToPtr(d.config.EnterpriseProjectId),\n\t\tCertificateId:       d.config.CertificateId,\n\t}\n\tshowCertificateResp, err := d.sdkClient.ShowCertificate(showCertificateReq)\n\td.logger.Debug(\"sdk request 'waf.ShowCertificate'\", slog.Any(\"request\", showCertificateReq), slog.Any(\"response\", showCertificateResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'waf.ShowCertificate': %w\", err)\n\t}\n\n\t// 更新证书\n\t// REF: https://support.huaweicloud.com/api-waf/UpdateCertificate.html\n\tupdateCertificateReq := &hcwafmodel.UpdateCertificateRequest{\n\t\tEnterpriseProjectId: lo.EmptyableToPtr(d.config.EnterpriseProjectId),\n\t\tCertificateId:       d.config.CertificateId,\n\t\tBody: &hcwafmodel.UpdateCertificateRequestBody{\n\t\t\tName:    *showCertificateResp.Name,\n\t\t\tContent: lo.ToPtr(certPEM),\n\t\t\tKey:     lo.ToPtr(privkeyPEM),\n\t\t},\n\t}\n\tupdateCertificateResp, err := d.sdkClient.UpdateCertificate(updateCertificateReq)\n\td.logger.Debug(\"sdk request 'waf.UpdateCertificate'\", slog.Any(\"request\", updateCertificateReq), slog.Any(\"response\", updateCertificateResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'waf.UpdateCertificate': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToCloudServer(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.Domain == \"\" {\n\t\treturn errors.New(\"config `domain` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 查询云模式防护域名列表，获取防护域名 ID\n\t// REF: https://support.huaweicloud.com/api-waf/ListHost.html\n\thostId := \"\"\n\tlistHostPage := 1\n\tlistHostPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistHostReq := &hcwafmodel.ListHostRequest{\n\t\t\tEnterpriseProjectId: lo.EmptyableToPtr(d.config.EnterpriseProjectId),\n\t\t\tHostname:            lo.ToPtr(strings.TrimPrefix(d.config.Domain, \"*\")),\n\t\t\tPage:                lo.ToPtr(int32(listHostPage)),\n\t\t\tPagesize:            lo.ToPtr(int32(listHostPageSize)),\n\t\t}\n\t\tlistHostResp, err := d.sdkClient.ListHost(listHostReq)\n\t\td.logger.Debug(\"sdk request 'waf.ListHost'\", slog.Any(\"request\", listHostReq), slog.Any(\"response\", listHostResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'waf.ListHost': %w\", err)\n\t\t}\n\n\t\tif listHostResp.Items == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, hostItem := range *listHostResp.Items {\n\t\t\tif strings.TrimPrefix(d.config.Domain, \"*\") == *hostItem.Hostname {\n\t\t\t\thostId = *hostItem.Id\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif len(*listHostResp.Items) < listHostPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tlistHostPage++\n\t}\n\tif hostId == \"\" {\n\t\treturn fmt.Errorf(\"could not find cloudserver host '%s'\", d.config.Domain)\n\t}\n\n\t// 更新云模式防护域名的配置\n\t// REF: https://support.huaweicloud.com/api-waf/UpdateHost.html\n\tupdateHostReq := &hcwafmodel.UpdateHostRequest{\n\t\tEnterpriseProjectId: lo.EmptyableToPtr(d.config.EnterpriseProjectId),\n\t\tInstanceId:          hostId,\n\t\tBody: &hcwafmodel.UpdateHostRequestBody{\n\t\t\tCertificateid:   lo.ToPtr(upres.CertId),\n\t\t\tCertificatename: lo.ToPtr(upres.CertName),\n\t\t},\n\t}\n\tupdateHostResp, err := d.sdkClient.UpdateHost(updateHostReq)\n\td.logger.Debug(\"sdk request 'waf.UpdateHost'\", slog.Any(\"request\", updateHostReq), slog.Any(\"response\", updateHostResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'waf.UpdateHost': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToPremiumHost(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.Domain == \"\" {\n\t\treturn errors.New(\"config `domain` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 查询独享模式域名列表，获取防护域名 ID\n\t// REF: https://support.huaweicloud.com/api-waf/ListPremiumHost.html\n\tvar hostId string\n\tlistPremiumHostPage := 1\n\tlistPremiumHostPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistPremiumHostReq := &hcwafmodel.ListPremiumHostRequest{\n\t\t\tEnterpriseProjectId: lo.EmptyableToPtr(d.config.EnterpriseProjectId),\n\t\t\tHostname:            lo.ToPtr(strings.TrimPrefix(d.config.Domain, \"*\")),\n\t\t\tPage:                lo.ToPtr(fmt.Sprintf(\"%d\", listPremiumHostPage)),\n\t\t\tPagesize:            lo.ToPtr(fmt.Sprintf(\"%d\", listPremiumHostPageSize)),\n\t\t}\n\t\tlistPremiumHostResp, err := d.sdkClient.ListPremiumHost(listPremiumHostReq)\n\t\td.logger.Debug(\"sdk request 'waf.ListPremiumHost'\", slog.Any(\"request\", listPremiumHostReq), slog.Any(\"response\", listPremiumHostResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'waf.ListPremiumHost': %w\", err)\n\t\t}\n\n\t\tif listPremiumHostResp.Items == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, hostItem := range *listPremiumHostResp.Items {\n\t\t\tif strings.TrimPrefix(d.config.Domain, \"*\") == *hostItem.Hostname {\n\t\t\t\thostId = *hostItem.Id\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif len(*listPremiumHostResp.Items) < listPremiumHostPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tlistPremiumHostPage++\n\t}\n\tif hostId == \"\" {\n\t\treturn fmt.Errorf(\"could not find premium host '%s'\", d.config.Domain)\n\t}\n\n\t// 修改独享模式域名配置\n\t// REF: https://support.huaweicloud.com/api-waf/UpdatePremiumHost.html\n\tupdatePremiumHostReq := &hcwafmodel.UpdatePremiumHostRequest{\n\t\tEnterpriseProjectId: lo.EmptyableToPtr(d.config.EnterpriseProjectId),\n\t\tHostId:              hostId,\n\t\tBody: &hcwafmodel.UpdatePremiumHostRequestBody{\n\t\t\tCertificateid:   lo.ToPtr(upres.CertId),\n\t\t\tCertificatename: lo.ToPtr(upres.CertName),\n\t\t},\n\t}\n\tupdatePremiumHostResp, err := d.sdkClient.UpdatePremiumHost(updatePremiumHostReq)\n\td.logger.Debug(\"sdk request 'waf.UpdatePremiumHost'\", slog.Any(\"request\", updatePremiumHostReq), slog.Any(\"response\", updatePremiumHostResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'waf.UpdatePremiumHost': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey, region string) (*internal.WafClient, error) {\n\tprojectId, err := getSDKProjectId(accessKeyId, secretAccessKey, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tauth, err := basic.NewCredentialsBuilder().\n\t\tWithAk(accessKeyId).\n\t\tWithSk(secretAccessKey).\n\t\tWithProjectId(projectId).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thcRegion, err := hcwafregion.SafeValueOf(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thcClient, err := hcwaf.WafClientBuilder().\n\t\tWithRegion(hcRegion).\n\t\tWithCredential(auth).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := internal.NewWafClient(hcClient)\n\treturn client, nil\n}\n\nfunc getSDKProjectId(accessKeyId, secretAccessKey, region string) (string, error) {\n\tauth, err := global.NewCredentialsBuilder().\n\t\tWithAk(accessKeyId).\n\t\tWithSk(secretAccessKey).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thcRegion, err := hciamregion.SafeValueOf(region)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thcClient, err := hciam.IamClientBuilder().\n\t\tWithRegion(hcRegion).\n\t\tWithCredential(auth).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tclient := hciam.NewIamClient(hcClient)\n\n\trequest := &hciamModel.KeystoneListProjectsRequest{\n\t\tName: &region,\n\t}\n\tresponse, err := client.KeystoneListProjects(request)\n\tif err != nil {\n\t\treturn \"\", err\n\t} else if response.Projects == nil || len(*response.Projects) == 0 {\n\t\treturn \"\", errors.New(\"huaweicloud: no project found\")\n\t}\n\n\treturn (*response.Projects)[0].Id, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf_test.go",
    "content": "package huaweicloudwaf_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-waf\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n\tfRegion          string\n\tfResourceType    string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"HUAWEICLOUDWAF_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fResourceType, argsPrefix+\"RESOURCETYPE\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./huaweicloud_waf_test.go -args \\\n\t--HUAWEICLOUDWAF_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--HUAWEICLOUDWAF_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--HUAWEICLOUDWAF_ACCESSKEYID=\"your-access-key-id\" \\\n\t--HUAWEICLOUDWAF_SECRETACCESSKEY=\"your-secret-access-key\" \\\n\t--HUAWEICLOUDWAF_REGION=\"cn-north-1\" \\\n\t--HUAWEICLOUDWAF_RESOURCETYPE=\"premium\" \\\n\t--HUAWEICLOUDWAF_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"RESOURCETYPE: %v\", fResourceType),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tSecretAccessKey: fSecretAccessKey,\n\t\t\tRegion:          fRegion,\n\t\t\tResourceType:    fResourceType,\n\t\t\tDomain:          fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/huaweicloud-waf/internal/client.go",
    "content": "package internal\n\nimport (\n\thttpclient \"github.com/huaweicloud/huaweicloud-sdk-go-v3/core\"\n\thwwaf \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1\"\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/model\"\n)\n\n// This is a partial copy of https://github.com/huaweicloud/huaweicloud-sdk-go-v3/blob/master/services/waf/v1/waf_client.go\n// to lightweight the vendor packages in the built binary.\ntype WafClient struct {\n\tHcClient *httpclient.HcHttpClient\n}\n\nfunc NewWafClient(hcClient *httpclient.HcHttpClient) *WafClient {\n\treturn &WafClient{HcClient: hcClient}\n}\n\nfunc (c *WafClient) ListHost(request *model.ListHostRequest) (*model.ListHostResponse, error) {\n\trequestDef := hwwaf.GenReqDefForListHost()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ListHostResponse), nil\n\t}\n}\n\nfunc (c *WafClient) ListPremiumHost(request *model.ListPremiumHostRequest) (*model.ListPremiumHostResponse, error) {\n\trequestDef := hwwaf.GenReqDefForListPremiumHost()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ListPremiumHostResponse), nil\n\t}\n}\n\nfunc (c *WafClient) ShowCertificate(request *model.ShowCertificateRequest) (*model.ShowCertificateResponse, error) {\n\trequestDef := hwwaf.GenReqDefForShowCertificate()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ShowCertificateResponse), nil\n\t}\n}\n\nfunc (c *WafClient) UpdateCertificate(request *model.UpdateCertificateRequest) (*model.UpdateCertificateResponse, error) {\n\trequestDef := hwwaf.GenReqDefForUpdateCertificate()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.UpdateCertificateResponse), nil\n\t}\n}\n\nfunc (c *WafClient) UpdateHost(request *model.UpdateHostRequest) (*model.UpdateHostResponse, error) {\n\trequestDef := hwwaf.GenReqDefForUpdateHost()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.UpdateHostResponse), nil\n\t}\n}\n\nfunc (c *WafClient) UpdatePremiumHost(request *model.UpdatePremiumHostRequest) (*model.UpdatePremiumHostResponse, error) {\n\trequestDef := hwwaf.GenReqDefForUpdatePremiumHost()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.UpdatePremiumHostResponse), nil\n\t}\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/jdcloud-alb/consts.go",
    "content": "package jdcloudalb\n\nconst (\n\t// 资源类型：部署到指定负载均衡器。\n\tRESOURCE_TYPE_LOADBALANCER = \"loadbalancer\"\n\t// 资源类型：部署到指定监听器。\n\tRESOURCE_TYPE_LISTENER = \"listener\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/jdcloud-alb/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\n\t\"github.com/jdcloud-api/jdcloud-sdk-go/core\"\n\tlb \"github.com/jdcloud-api/jdcloud-sdk-go/services/lb/apis\"\n)\n\n// This is a partial copy of https://github.com/jdcloud-api/jdcloud-sdk-go/blob/master/services/lb/client/LbClient.go\n// to lightweight the vendor packages in the built binary.\ntype LbClient struct {\n\tcore.JDCloudClient\n}\n\nfunc NewLbClient(credential *core.Credential) *LbClient {\n\tif credential == nil {\n\t\treturn nil\n\t}\n\n\tconfig := core.NewConfig()\n\tconfig.SetEndpoint(\"lb.jdcloud-api.com\")\n\n\treturn &LbClient{\n\t\tcore.JDCloudClient{\n\t\t\tCredential:  *credential,\n\t\t\tConfig:      *config,\n\t\t\tServiceName: \"lb\",\n\t\t\tRevision:    \"0.6.6\",\n\t\t\tLogger:      core.NewDefaultLogger(core.LogInfo),\n\t\t},\n\t}\n}\n\nfunc (c *LbClient) DescribeListener(request *lb.DescribeListenerRequest) (*lb.DescribeListenerResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"Request object is nil.\")\n\t}\n\n\tresp, err := c.Send(request, c.ServiceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjdResp := &lb.DescribeListenerResponse{}\n\terr = json.Unmarshal(resp, jdResp)\n\tif err != nil {\n\t\tc.Logger.Log(core.LogError, \"Unmarshal json failed, resp: %s\", string(resp))\n\t\treturn nil, err\n\t}\n\n\treturn jdResp, err\n}\n\nfunc (c *LbClient) DescribeListeners(request *lb.DescribeListenersRequest) (*lb.DescribeListenersResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"Request object is nil.\")\n\t}\n\n\tresp, err := c.Send(request, c.ServiceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjdResp := &lb.DescribeListenersResponse{}\n\terr = json.Unmarshal(resp, jdResp)\n\tif err != nil {\n\t\tc.Logger.Log(core.LogError, \"Unmarshal json failed, resp: %s\", string(resp))\n\t\treturn nil, err\n\t}\n\n\treturn jdResp, err\n}\n\nfunc (c *LbClient) DescribeLoadBalancer(request *lb.DescribeLoadBalancerRequest) (*lb.DescribeLoadBalancerResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"Request object is nil.\")\n\t}\n\n\tresp, err := c.Send(request, c.ServiceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjdResp := &lb.DescribeLoadBalancerResponse{}\n\terr = json.Unmarshal(resp, jdResp)\n\tif err != nil {\n\t\tc.Logger.Log(core.LogError, \"Unmarshal json failed, resp: %s\", string(resp))\n\t\treturn nil, err\n\t}\n\n\treturn jdResp, err\n}\n\nfunc (c *LbClient) UpdateListener(request *lb.UpdateListenerRequest) (*lb.UpdateListenerResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"Request object is nil.\")\n\t}\n\n\tresp, err := c.Send(request, c.ServiceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjdResp := &lb.UpdateListenerResponse{}\n\terr = json.Unmarshal(resp, jdResp)\n\tif err != nil {\n\t\tc.Logger.Log(core.LogError, \"Unmarshal json failed, resp: %s\", string(resp))\n\t\treturn nil, err\n\t}\n\n\treturn jdResp, err\n}\n\nfunc (c *LbClient) UpdateListenerCertificates(request *lb.UpdateListenerCertificatesRequest) (*lb.UpdateListenerCertificatesResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"Request object is nil.\")\n\t}\n\n\tresp, err := c.Send(request, c.ServiceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjdResp := &lb.UpdateListenerCertificatesResponse{}\n\terr = json.Unmarshal(resp, jdResp)\n\tif err != nil {\n\t\tc.Logger.Log(core.LogError, \"Unmarshal json failed, resp: %s\", string(resp))\n\t\treturn nil, err\n\t}\n\n\treturn jdResp, err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/jdcloud-alb/jdcloud_alb.go",
    "content": "package jdcloudalb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\tjdcore \"github.com/jdcloud-api/jdcloud-sdk-go/core\"\n\tjdcommon \"github.com/jdcloud-api/jdcloud-sdk-go/services/common/models\"\n\tjdlb \"github.com/jdcloud-api/jdcloud-sdk-go/services/lb/apis\"\n\tjdlbmodel \"github.com/jdcloud-api/jdcloud-sdk-go/services/lb/models\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/jdcloud-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-alb/internal\"\n)\n\ntype DeployerConfig struct {\n\t// 京东云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 京东云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 京东云地域 ID。\n\tRegionId string `json:\"regionId\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 负载均衡器 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。\n\tLoadbalancerId string `json:\"loadbalancerId,omitempty\"`\n\t// 监听器 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。\n\tListenerId string `json:\"listenerId,omitempty\"`\n\t// SNI 域名（支持泛域名）。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。\n\tDomain string `json:\"domain,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.LbClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_LOADBALANCER:\n\t\tif err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_LISTENER:\n\t\tif err := d.deployToListener(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\n\t// 查询负载均衡器详情\n\t// REF: https://docs.jdcloud.com/cn/load-balancer/api/describeloadbalancer\n\tdescribeLoadBalancerReq := jdlb.NewDescribeLoadBalancerRequestWithoutParam()\n\tdescribeLoadBalancerReq.SetRegionId(d.config.RegionId)\n\tdescribeLoadBalancerReq.SetLoadBalancerId(d.config.LoadbalancerId)\n\tdescribeLoadBalancerResp, err := d.sdkClient.DescribeLoadBalancer(describeLoadBalancerReq)\n\td.logger.Debug(\"sdk request 'lb.DescribeLoadBalancer'\", slog.Any(\"request\", describeLoadBalancerReq), slog.Any(\"response\", describeLoadBalancerResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'lb.DescribeLoadBalancer': %w\", err)\n\t}\n\n\t// 查询监听器列表\n\t// REF: https://docs.jdcloud.com/cn/load-balancer/api/describelisteners\n\tlistenerIds := make([]string, 0)\n\tdescribeListenersPageNumber := 1\n\tdescribeListenersPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeListenersReq := jdlb.NewDescribeListenersRequestWithoutParam()\n\t\tdescribeListenersReq.SetRegionId(d.config.RegionId)\n\t\tdescribeListenersReq.SetFilters([]jdcommon.Filter{{Name: \"loadBalancerId\", Values: []string{d.config.LoadbalancerId}}})\n\t\tdescribeListenersReq.SetPageSize(describeListenersPageNumber)\n\t\tdescribeListenersReq.SetPageSize(describeListenersPageSize)\n\t\tdescribeListenersResp, err := d.sdkClient.DescribeListeners(describeListenersReq)\n\t\td.logger.Debug(\"sdk request 'lb.DescribeListeners'\", slog.Any(\"request\", describeListenersReq), slog.Any(\"response\", describeListenersResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'lb.DescribeListeners': %w\", err)\n\t\t}\n\n\t\tfor _, listener := range describeListenersResp.Result.Listeners {\n\t\t\tif strings.EqualFold(listener.Protocol, \"https\") || strings.EqualFold(listener.Protocol, \"tls\") {\n\t\t\t\tlistenerIds = append(listenerIds, listener.ListenerId)\n\t\t\t}\n\t\t}\n\n\t\tif len(describeListenersResp.Result.Listeners) < describeListenersPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeListenersPageNumber++\n\t}\n\n\t// 遍历更新监听器证书\n\tif len(listenerIds) == 0 {\n\t\td.logger.Info(\"no listeners to deploy\")\n\t} else {\n\t\td.logger.Info(\"found https/tls listeners to deploy\", slog.Any(\"listenerIds\", listenerIds))\n\n\t\tvar errs []error\n\n\t\tfor _, listenerId := range listenerIds {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error {\n\tif d.config.ListenerId == \"\" {\n\t\treturn errors.New(\"config `listenerId` is required\")\n\t}\n\n\t// 更新监听器证书\n\tif err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {\n\t// 查询监听器详情\n\t// REF: https://docs.jdcloud.com/cn/load-balancer/api/describelistener\n\tdescribeListenerReq := jdlb.NewDescribeListenerRequestWithoutParam()\n\tdescribeListenerReq.SetRegionId(d.config.RegionId)\n\tdescribeListenerReq.SetListenerId(cloudListenerId)\n\tdescribeListenerResp, err := d.sdkClient.DescribeListener(describeListenerReq)\n\td.logger.Debug(\"sdk request 'lb.DescribeListener'\", slog.Any(\"request\", describeListenerReq), slog.Any(\"response\", describeListenerResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'lb.DescribeListener': %w\", err)\n\t}\n\n\tif d.config.Domain == \"\" {\n\t\t// 未指定 SNI，只需部署到监听器\n\n\t\t// 修改监听器信息\n\t\t// REF: https://docs.jdcloud.com/cn/load-balancer/api/updatelistener\n\t\tupdateListenerReq := jdlb.NewUpdateListenerRequestWithoutParam()\n\t\tupdateListenerReq.SetRegionId(d.config.RegionId)\n\t\tupdateListenerReq.SetListenerId(cloudListenerId)\n\t\tupdateListenerReq.SetCertificateSpecs([]jdlbmodel.CertificateSpec{{CertificateId: cloudCertId}})\n\t\tupdateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq)\n\t\td.logger.Debug(\"sdk request 'lb.UpdateListener'\", slog.Any(\"request\", updateListenerReq), slog.Any(\"response\", updateListenerResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'lb.UpdateListener': %w\", err)\n\t\t}\n\t} else {\n\t\t// 指定 SNI，需部署到扩展证书\n\n\t\textCertSpecs := lo.Filter(describeListenerResp.Result.Listener.ExtensionCertificateSpecs, func(extCertSpec jdlbmodel.ExtensionCertificateSpec, _ int) bool {\n\t\t\treturn extCertSpec.Domain == d.config.Domain\n\t\t})\n\t\tif len(extCertSpecs) == 0 {\n\t\t\treturn errors.New(\"could not find any extension certificates\")\n\t\t}\n\n\t\t// 批量修改扩展证书\n\t\t// REF: https://docs.jdcloud.com/cn/load-balancer/api/updatelistenercertificates\n\t\tupdateListenerCertificatesReq := jdlb.NewUpdateListenerCertificatesRequestWithoutParam()\n\t\tupdateListenerCertificatesReq.SetRegionId(d.config.RegionId)\n\t\tupdateListenerCertificatesReq.SetListenerId(cloudListenerId)\n\t\tupdateListenerCertificatesReq.SetCertificates(lo.Map(extCertSpecs, func(extCertSpec jdlbmodel.ExtensionCertificateSpec, _ int) jdlbmodel.ExtCertificateUpdateSpec {\n\t\t\treturn jdlbmodel.ExtCertificateUpdateSpec{\n\t\t\t\tCertificateBindId: extCertSpec.CertificateBindId,\n\t\t\t\tCertificateId:     &cloudCertId,\n\t\t\t\tDomain:            &extCertSpec.Domain,\n\t\t\t}\n\t\t}))\n\t\tupdateListenerCertificatesResp, err := d.sdkClient.UpdateListenerCertificates(updateListenerCertificatesReq)\n\t\td.logger.Debug(\"sdk request 'lb.UpdateListenerCertificates'\", slog.Any(\"request\", updateListenerCertificatesReq), slog.Any(\"response\", updateListenerCertificatesResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'lb.UpdateListenerCertificates': %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret string) (*internal.LbClient, error) {\n\tclientCredentials := jdcore.NewCredentials(accessKeyId, accessKeySecret)\n\tclient := internal.NewLbClient(clientCredentials)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/jdcloud-alb/jdcloud_alb_test.go",
    "content": "package jdcloudalb_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-alb\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegionId        string\n\tfLoadbalancerId  string\n\tfListenerId      string\n)\n\nfunc init() {\n\targsPrefix := \"JDCLOUDALB_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegionId, argsPrefix+\"REGIONID\", \"\", \"\")\n\tflag.StringVar(&fLoadbalancerId, argsPrefix+\"LOADBALANCERID\", \"\", \"\")\n\tflag.StringVar(&fListenerId, argsPrefix+\"LISTENERID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./jdcloud_alb_test.go -args \\\n\t--JDCLOUDALB_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--JDCLOUDALB_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--JDCLOUDALB_ACCESSKEYID=\"your-access-key-id\" \\\n\t--JDCLOUDALB_ACCESSKEYSECRET=\"your-secret-access-key\" \\\n\t--JDCLOUDALB_REGION_ID=\"cn-north-1\" \\\n\t--JDCLOUDALB_LOADBALANCERID=\"your-alb-loadbalancer-id\" \\\n\t--JDCLOUDALB_LISTENERID=\"your-alb-listener-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy_ToLoadbalancer\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGIONID: %v\", fRegionId),\n\t\t\tfmt.Sprintf(\"LOADBALANCERID: %v\", fLoadbalancerId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegionId:        fRegionId,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LOADBALANCER,\n\t\t\tLoadbalancerId:  fLoadbalancerId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n\n\tt.Run(\"Deploy_ToListener\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGIONID: %v\", fRegionId),\n\t\t\tfmt.Sprintf(\"LISTENERID: %v\", fListenerId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegionId:        fRegionId,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LISTENER,\n\t\t\tListenerId:      fListenerId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/jdcloud-cdn/consts.go",
    "content": "package jdcloudcdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/jdcloud-cdn/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\n\t\"github.com/jdcloud-api/jdcloud-sdk-go/core\"\n\tcdn \"github.com/jdcloud-api/jdcloud-sdk-go/services/cdn/apis\"\n)\n\n// This is a partial copy of https://github.com/jdcloud-api/jdcloud-sdk-go/blob/master/services/cdn/client/CdnClient.go\n// to lightweight the vendor packages in the built binary.\ntype CdnClient struct {\n\tcore.JDCloudClient\n}\n\nfunc NewCdnClient(credential *core.Credential) *CdnClient {\n\tif credential == nil {\n\t\treturn nil\n\t}\n\n\tconfig := core.NewConfig()\n\tconfig.SetEndpoint(\"cdn.jdcloud-api.com\")\n\n\treturn &CdnClient{\n\t\tcore.JDCloudClient{\n\t\t\tCredential:  *credential,\n\t\t\tConfig:      *config,\n\t\t\tServiceName: \"cdn\",\n\t\t\tRevision:    \"0.10.47\",\n\t\t\tLogger:      core.NewDummyLogger(),\n\t\t},\n\t}\n}\n\nfunc (c *CdnClient) GetDomainList(request *cdn.GetDomainListRequest) (*cdn.GetDomainListResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"Request object is nil.\")\n\t}\n\n\tresp, err := c.Send(request, c.ServiceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjdResp := &cdn.GetDomainListResponse{}\n\terr = json.Unmarshal(resp, jdResp)\n\tif err != nil {\n\t\tc.Logger.Log(core.LogError, \"Unmarshal json failed, resp: %s\", string(resp))\n\t\treturn nil, err\n\t}\n\n\treturn jdResp, err\n}\n\nfunc (c *CdnClient) QueryDomainConfig(request *cdn.QueryDomainConfigRequest) (*cdn.QueryDomainConfigResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"Request object is nil.\")\n\t}\n\n\tresp, err := c.Send(request, c.ServiceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjdResp := &cdn.QueryDomainConfigResponse{}\n\terr = json.Unmarshal(resp, jdResp)\n\tif err != nil {\n\t\tc.Logger.Log(core.LogError, \"Unmarshal json failed, resp: %s\", string(resp))\n\t\treturn nil, err\n\t}\n\n\treturn jdResp, err\n}\n\nfunc (c *CdnClient) SetHttpType(request *cdn.SetHttpTypeRequest) (*cdn.SetHttpTypeResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"Request object is nil.\")\n\t}\n\n\tresp, err := c.Send(request, c.ServiceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjdResp := &cdn.SetHttpTypeResponse{}\n\terr = json.Unmarshal(resp, jdResp)\n\tif err != nil {\n\t\tc.Logger.Log(core.LogError, \"Unmarshal json failed, resp: %s\", string(resp))\n\t\treturn nil, err\n\t}\n\n\treturn jdResp, err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn.go",
    "content": "package jdcloudcdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\tjdcore \"github.com/jdcloud-api/jdcloud-sdk-go/core\"\n\tjdcdn \"github.com/jdcloud-api/jdcloud-sdk-go/services/cdn/apis\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/jdcloud-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-cdn/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 京东云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 京东云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.CdnClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain)\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no cdn domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found cdn domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// REF: https://docs.jdcloud.com/cn/cdn/api/getdomainlist\n\tgetDomainListPageNumber := 1\n\tgetDomainListPageSize := 50\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tgetDomainListReq := jdcdn.NewGetDomainListRequestWithoutParam()\n\t\tgetDomainListReq.SetPageNumber(getDomainListPageNumber)\n\t\tgetDomainListReq.SetPageSize(getDomainListPageSize)\n\t\tgetDomainListResp, err := d.sdkClient.GetDomainList(getDomainListReq)\n\t\td.logger.Debug(\"sdk request 'cdn.GetDomainList'\", slog.Any(\"request\", getDomainListReq), slog.Any(\"response\", getDomainListResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.GetDomainList': %w\", err)\n\t\t}\n\n\t\tignoredStatuses := []string{\"offline\"}\n\t\tfor _, domainItem := range getDomainListResp.Result.Domains {\n\t\t\tif lo.Contains(ignoredStatuses, domainItem.Status) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdomains = append(domains, domainItem.Domain)\n\t\t}\n\n\t\tif len(getDomainListResp.Result.Domains) < getDomainListPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tgetDomainListPageNumber++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error {\n\t// 查询域名配置信息\n\t// REF: https://docs.jdcloud.com/cn/cdn/api/querydomainconfig\n\tqueryDomainConfigReq := jdcdn.NewQueryDomainConfigRequestWithoutParam()\n\tqueryDomainConfigReq.SetDomain(domain)\n\tqueryDomainConfigResp, err := d.sdkClient.QueryDomainConfig(queryDomainConfigReq)\n\td.logger.Debug(\"sdk request 'cdn.QueryDomainConfig'\", slog.Any(\"request\", queryDomainConfigReq), slog.Any(\"response\", queryDomainConfigResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.QueryDomainConfig': %w\", err)\n\t}\n\n\t// 设置通讯协议\n\t// REF: https://docs.jdcloud.com/cn/cdn/api/sethttptype\n\tsetHttpTypeReq := jdcdn.NewSetHttpTypeRequestWithoutParam()\n\tsetHttpTypeReq.SetDomain(domain)\n\tsetHttpTypeReq.SetHttpType(\"https\")\n\tsetHttpTypeReq.SetCertFrom(\"ssl\")\n\tsetHttpTypeReq.SetSslCertId(cloudCertId)\n\tsetHttpTypeReq.SetJumpType(queryDomainConfigResp.Result.HttpsJumpType)\n\tsetHttpTypeResp, err := d.sdkClient.SetHttpType(setHttpTypeReq)\n\td.logger.Debug(\"sdk request 'cdn.QueryDomainConfig'\", slog.Any(\"request\", setHttpTypeReq), slog.Any(\"response\", setHttpTypeResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.SetHttpType': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret string) (*internal.CdnClient, error) {\n\tclientCredentials := jdcore.NewCredentials(accessKeyId, accessKeySecret)\n\tclient := internal.NewCdnClient(clientCredentials)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn_test.go",
    "content": "package jdcloudcdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-cdn\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"JDCLOUDCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./jdcloud_cdn_test.go -args \\\n\t--JDCLOUDCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--JDCLOUDCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--JDCLOUDCDN_ACCESSKEYID=\"your-access-key-id\" \\\n\t--JDCLOUDCDN_ACCESSKEYSECRET=\"your-secret-access-key\" \\\n\t--JDCLOUDCDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tAccessKeySecret:    fAccessKeySecret,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/jdcloud-live/consts.go",
    "content": "package jdcloudlive\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/jdcloud-live/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\n\t\"github.com/jdcloud-api/jdcloud-sdk-go/core\"\n\tlive \"github.com/jdcloud-api/jdcloud-sdk-go/services/live/apis\"\n)\n\n// This is a partial copy of https://github.com/jdcloud-api/jdcloud-sdk-go/blob/master/services/live/client/LiveClient.go\n// to lightweight the vendor packages in the built binary.\ntype LiveClient struct {\n\tcore.JDCloudClient\n}\n\nfunc NewLiveClient(credential *core.Credential) *LiveClient {\n\tif credential == nil {\n\t\treturn nil\n\t}\n\n\tconfig := core.NewConfig()\n\tconfig.SetEndpoint(\"live.jdcloud-api.com\")\n\n\treturn &LiveClient{\n\t\tcore.JDCloudClient{\n\t\t\tCredential:  *credential,\n\t\t\tConfig:      *config,\n\t\t\tServiceName: \"live\",\n\t\t\tRevision:    \"1.0.22\",\n\t\t\tLogger:      core.NewDummyLogger(),\n\t\t},\n\t}\n}\n\nfunc (c *LiveClient) DescribeLiveDomains(request *live.DescribeLiveDomainsRequest) (*live.DescribeLiveDomainsResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"Request object is nil.\")\n\t}\n\tresp, err := c.Send(request, c.ServiceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjdResp := &live.DescribeLiveDomainsResponse{}\n\terr = json.Unmarshal(resp, jdResp)\n\tif err != nil {\n\t\tc.Logger.Log(core.LogError, \"Unmarshal json failed, resp: %s\", string(resp))\n\t\treturn nil, err\n\t}\n\n\treturn jdResp, err\n}\n\nfunc (c *LiveClient) SetLiveDomainCertificate(request *live.SetLiveDomainCertificateRequest) (*live.SetLiveDomainCertificateResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"Request object is nil.\")\n\t}\n\n\tresp, err := c.Send(request, c.ServiceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjdResp := &live.SetLiveDomainCertificateResponse{}\n\terr = json.Unmarshal(resp, jdResp)\n\tif err != nil {\n\t\tc.Logger.Log(core.LogError, \"Unmarshal json failed, resp: %s\", string(resp))\n\t\treturn nil, err\n\t}\n\n\treturn jdResp, err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/jdcloud-live/jdcloud_live.go",
    "content": "package jdcloudlive\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\tjdcore \"github.com/jdcloud-api/jdcloud-sdk-go/core\"\n\tjdlive \"github.com/jdcloud-api/jdcloud-sdk-go/services/live/apis\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-live/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype DeployerConfig struct {\n\t// 京东云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 京东云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 直播播放域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *internal.LiveClient\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no live domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found live domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, certPEM, privkeyPEM); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// REF: https://docs.jdcloud.com/cn/live-video/api/describelivedomains\n\tdescribeLiveDomainsPageNumber := 1\n\tdescribeLiveDomainsPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeLiveDomainsReq := jdlive.NewDescribeLiveDomainsRequestWithoutParam()\n\t\tdescribeLiveDomainsReq.SetPageNum(describeLiveDomainsPageNumber)\n\t\tdescribeLiveDomainsReq.SetPageSize(describeLiveDomainsPageSize)\n\t\tdescribeLiveDomainsResp, err := d.sdkClient.DescribeLiveDomains(describeLiveDomainsReq)\n\t\td.logger.Debug(\"sdk request 'live.DescribeLiveDomainsRequest'\", slog.Any(\"request\", describeLiveDomainsReq), slog.Any(\"response\", describeLiveDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'live.DescribeLiveDomainsRequest': %w\", err)\n\t\t}\n\n\t\tignoredStatuses := []string{\"offline\", \"checking\", \"check_failed\"}\n\t\tfor _, domainItem := range describeLiveDomainsResp.Result.DomainDetails {\n\t\t\tfor _, playDomainItem := range domainItem.PlayDomains {\n\t\t\t\tif lo.Contains(ignoredStatuses, playDomainItem.DomainStatus) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tdomains = append(domains, playDomainItem.PlayDomain)\n\t\t\t}\n\t\t}\n\n\t\tif len(describeLiveDomainsResp.Result.DomainDetails) < describeLiveDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeLiveDomainsPageNumber++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, certPEM, privkeyPEM string) error {\n\t// 设置直播证书\n\t// REF: https://docs.jdcloud.com/cn/live-video/api/setlivedomaincertificate\n\tsetLiveDomainCertificateReq := jdlive.NewSetLiveDomainCertificateRequestWithoutParam()\n\tsetLiveDomainCertificateReq.SetPlayDomain(domain)\n\tsetLiveDomainCertificateReq.SetCertStatus(\"on\")\n\tsetLiveDomainCertificateReq.SetCert(certPEM)\n\tsetLiveDomainCertificateReq.SetKey(privkeyPEM)\n\tsetLiveDomainCertificateResp, err := d.sdkClient.SetLiveDomainCertificate(setLiveDomainCertificateReq)\n\td.logger.Debug(\"sdk request 'live.SetLiveDomainCertificate'\", slog.Any(\"request\", setLiveDomainCertificateReq), slog.Any(\"response\", setLiveDomainCertificateResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'live.SetLiveDomainCertificate': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret string) (*internal.LiveClient, error) {\n\tclientCredentials := jdcore.NewCredentials(accessKeyId, accessKeySecret)\n\tclient := internal.NewLiveClient(clientCredentials)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/jdcloud-live/jdcloud_live_test.go",
    "content": "package jdcloudlive_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-live\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"JDCLOUDLIVE_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./jdcloud_live_test.go -args \\\n\t--JDCLOUDLIVE_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--JDCLOUDLIVE_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--JDCLOUDLIVE_ACCESSKEYID=\"your-access-key-id\" \\\n\t--JDCLOUDLIVE_ACCESSKEYSECRET=\"your-secret-access-key\" \\\n\t--JDCLOUDLIVE_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tAccessKeySecret:    fAccessKeySecret,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/jdcloud-vod/consts.go",
    "content": "package jdcloudvod\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/jdcloud-vod/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\n\t\"github.com/jdcloud-api/jdcloud-sdk-go/core\"\n\tvod \"github.com/jdcloud-api/jdcloud-sdk-go/services/vod/apis\"\n)\n\n// This is a partial copy of https://github.com/jdcloud-api/jdcloud-sdk-go/blob/master/services/vod/client/VodClient.go\n// to lightweight the vendor packages in the built binary.\ntype VodClient struct {\n\tcore.JDCloudClient\n}\n\nfunc NewVodClient(credential *core.Credential) *VodClient {\n\tif credential == nil {\n\t\treturn nil\n\t}\n\n\tconfig := core.NewConfig()\n\tconfig.SetEndpoint(\"vod.jdcloud-api.com\")\n\n\treturn &VodClient{\n\t\tcore.JDCloudClient{\n\t\t\tCredential:  *credential,\n\t\t\tConfig:      *config,\n\t\t\tServiceName: \"vod\",\n\t\t\tRevision:    \"1.2.1\",\n\t\t\tLogger:      core.NewDummyLogger(),\n\t\t},\n\t}\n}\n\nfunc (c *VodClient) ListDomains(request *vod.ListDomainsRequest) (*vod.ListDomainsResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"Request object is nil.\")\n\t}\n\n\tresp, err := c.Send(request, c.ServiceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjdResp := &vod.ListDomainsResponse{}\n\terr = json.Unmarshal(resp, jdResp)\n\tif err != nil {\n\t\tc.Logger.Log(core.LogError, \"Unmarshal json failed, resp: %s\", string(resp))\n\t\treturn nil, err\n\t}\n\n\treturn jdResp, err\n}\n\nfunc (c *VodClient) GetHttpSsl(request *vod.GetHttpSslRequest) (*vod.GetHttpSslResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"Request object is nil.\")\n\t}\n\n\tresp, err := c.Send(request, c.ServiceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjdResp := &vod.GetHttpSslResponse{}\n\terr = json.Unmarshal(resp, jdResp)\n\tif err != nil {\n\t\tc.Logger.Log(core.LogError, \"Unmarshal json failed, resp: %s\", string(resp))\n\t\treturn nil, err\n\t}\n\n\treturn jdResp, err\n}\n\nfunc (c *VodClient) SetHttpSsl(request *vod.SetHttpSslRequest) (*vod.SetHttpSslResponse, error) {\n\tif request == nil {\n\t\treturn nil, errors.New(\"Request object is nil.\")\n\t}\n\n\tresp, err := c.Send(request, c.ServiceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjdResp := &vod.SetHttpSslResponse{}\n\terr = json.Unmarshal(resp, jdResp)\n\tif err != nil {\n\t\tc.Logger.Log(core.LogError, \"Unmarshal json failed, resp: %s\", string(resp))\n\t\treturn nil, err\n\t}\n\n\treturn jdResp, err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/jdcloud-vod/jdcloud_vod.go",
    "content": "package jdcloudvod\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"time\"\n\n\tjdcore \"github.com/jdcloud-api/jdcloud-sdk-go/core\"\n\tjdvod \"github.com/jdcloud-api/jdcloud-sdk-go/services/vod/apis\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-vod/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype DeployerConfig struct {\n\t// 京东云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 京东云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 点播加速域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *internal.VodClient\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no vod domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found vod domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, certPEM, privkeyPEM); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// REF: https://docs.jdcloud.com/cn/video-on-demand/api/listdomains\n\tlistDomainsPageNumber := 1\n\tlistDomainsPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistDomainsReq := jdvod.NewListDomainsRequestWithoutParam()\n\t\tlistDomainsReq.SetPageNumber(listDomainsPageNumber)\n\t\tlistDomainsReq.SetPageSize(listDomainsPageSize)\n\t\tlistDomainsResp, err := d.sdkClient.ListDomains(listDomainsReq)\n\t\td.logger.Debug(\"sdk request 'vod.ListDomains'\", slog.Any(\"request\", listDomainsReq), slog.Any(\"response\", listDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'vod.ListDomains': %w\", err)\n\t\t}\n\n\t\tignoredStatuses := []string{\"init\", \"stopped\"}\n\t\tfor _, domainItem := range listDomainsResp.Result.Content {\n\t\t\tif lo.Contains(ignoredStatuses, domainItem.Status) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdomains = append(domains, domainItem.Name)\n\t\t}\n\n\t\tif len(listDomainsResp.Result.Content) < listDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tlistDomainsPageNumber++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, certPEM, privkeyPEM string) error {\n\t// 获取域名 ID\n\tdomainId, err := d.findDomainIdByDomain(ctx, domain)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 查询域名 SSL 配置\n\t// REF: https://docs.jdcloud.com/cn/video-on-demand/api/gethttpssl\n\tgetHttpSslReq := jdvod.NewGetHttpSslRequestWithoutParam()\n\tgetHttpSslReq.SetDomainId(domainId)\n\tgetHttpSslResp, err := d.sdkClient.GetHttpSsl(getHttpSslReq)\n\td.logger.Debug(\"sdk request 'vod.GetHttpSsl'\", slog.Any(\"request\", getHttpSslReq), slog.Any(\"response\", getHttpSslResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'vod.GetHttpSsl': %w\", err)\n\t}\n\n\t// 设置域名 SSL 配置\n\t// REF: https://docs.jdcloud.com/cn/video-on-demand/api/sethttpssl\n\tsetHttpSslReq := jdvod.NewSetHttpSslRequestWithoutParam()\n\tsetHttpSslReq.SetDomainId(domainId)\n\tsetHttpSslReq.SetTitle(fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli()))\n\tsetHttpSslReq.SetSslCert(certPEM)\n\tsetHttpSslReq.SetSslKey(privkeyPEM)\n\tsetHttpSslReq.SetSource(\"default\")\n\tsetHttpSslReq.SetJumpType(getHttpSslResp.Result.JumpType)\n\tsetHttpSslReq.SetEnabled(true)\n\tsetHttpSslResp, err := d.sdkClient.SetHttpSsl(setHttpSslReq)\n\td.logger.Debug(\"sdk request 'vod.SetHttpSsl'\", slog.Any(\"request\", setHttpSslReq), slog.Any(\"response\", setHttpSslResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'vod.SetHttpSsl': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) findDomainIdByDomain(ctx context.Context, domain string) (int, error) {\n\t// 查询域名列表\n\t// REF: https://docs.jdcloud.com/cn/video-on-demand/api/listdomains\n\tlistDomainsPageNumber := 1\n\tlistDomainsPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn 0, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistDomainsReq := jdvod.NewListDomainsRequestWithoutParam()\n\t\tlistDomainsReq.SetPageNumber(listDomainsPageNumber)\n\t\tlistDomainsReq.SetPageSize(listDomainsPageSize)\n\t\tlistDomainsResp, err := d.sdkClient.ListDomains(listDomainsReq)\n\t\td.logger.Debug(\"sdk request 'vod.ListDomains'\", slog.Any(\"request\", listDomainsReq), slog.Any(\"response\", listDomainsResp))\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"failed to execute sdk request 'vod.ListDomains': %w\", err)\n\t\t}\n\n\t\tfor _, domainItem := range listDomainsResp.Result.Content {\n\t\t\tif domainItem.Name == domain {\n\t\t\t\tdomainId, _ := strconv.Atoi(domainItem.Id)\n\t\t\t\treturn domainId, nil\n\t\t\t}\n\t\t}\n\n\t\tif len(listDomainsResp.Result.Content) < listDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tlistDomainsPageNumber++\n\t}\n\n\treturn 0, fmt.Errorf(\"could not find domain '%s'\", domain)\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret string) (*internal.VodClient, error) {\n\tclientCredentials := jdcore.NewCredentials(accessKeyId, accessKeySecret)\n\tclient := internal.NewVodClient(clientCredentials)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/jdcloud-vod/jdcloud_vod_test.go",
    "content": "package jdcloudvod_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-vod\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"JDCLOUDVOD_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./jdcloud_vod_test.go -args \\\n\t--JDCLOUDVOD_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--JDCLOUDVOD_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--JDCLOUDVOD_ACCESSKEYID=\"your-access-key-id\" \\\n\t--JDCLOUDVOD_ACCESSKEYSECRET=\"your-secret-access-key\" \\\n\t--JDCLOUDVOD_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tAccessKeySecret:    fAccessKeySecret,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/k8s-secret/k8s_secret.go",
    "content": "package k8ssecret\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\tk8score \"k8s.io/api/core/v1\"\n\tk8serrors \"k8s.io/apimachinery/pkg/api/errors\"\n\tk8smeta \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/client-go/kubernetes\"\n\t\"k8s.io/client-go/rest\"\n\t\"k8s.io/client-go/tools/clientcmd\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype DeployerConfig struct {\n\t// kubeconfig 文件内容。\n\tKubeConfig string `json:\"kubeConfig,omitempty\"`\n\t// Kubernetes 命名空间。\n\tNamespace string `json:\"namespace,omitempty\"`\n\t// Kubernetes Secret 名称。\n\tSecretName string `json:\"secretName\"`\n\t// Kubernetes Secret 类型。\n\tSecretType string `json:\"secretType\"`\n\t// Kubernetes Secret 中用于存放证书的 Key。\n\tSecretDataKeyForCrt string `json:\"secretDataKeyForCrt,omitempty\"`\n\t// Kubernetes Secret 中用于存放私钥的 Key。\n\tSecretDataKeyForKey string `json:\"secretDataKeyForKey,omitempty\"`\n\t// Kubernetes Secret 注解。\n\tSecretAnnotations map[string]string `json:\"secretAnnotations,omitempty\"`\n\t// Kubernetes Secret 标签。\n\tSecretLabels map[string]string `json:\"secretLabels,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig *DeployerConfig\n\tlogger *slog.Logger\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\treturn &Deployer{\n\t\tlogger: slog.Default(),\n\t\tconfig: config,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.Namespace == \"\" {\n\t\treturn nil, errors.New(\"config `namespace` is required\")\n\t}\n\tif d.config.SecretName == \"\" {\n\t\treturn nil, errors.New(\"config `secretName` is required\")\n\t}\n\tif d.config.SecretType == \"\" {\n\t\treturn nil, errors.New(\"config `secretType` is required\")\n\t}\n\tif d.config.SecretDataKeyForCrt == \"\" {\n\t\treturn nil, errors.New(\"config `secretDataKeyForCrt` is required\")\n\t}\n\tif d.config.SecretDataKeyForKey == \"\" {\n\t\treturn nil, errors.New(\"config `secretDataKeyForKey` is required\")\n\t}\n\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 连接\n\tclient, err := createK8sClient(d.config.KubeConfig)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create kubernetes client: %w\", err)\n\t}\n\n\tvar secretPayload *k8score.Secret\n\tsecretAnnotations := map[string]string{\n\t\t\"certimate/common-name\":       certX509.Subject.CommonName,\n\t\t\"certimate/subject-sn\":        certX509.Subject.SerialNumber,\n\t\t\"certimate/subject-alt-names\": strings.Join(certX509.DNSNames, \",\"),\n\t\t\"certimate/issuer-sn\":         certX509.Issuer.SerialNumber,\n\t\t\"certimate/issuer-org\":        strings.Join(certX509.Issuer.Organization, \",\"),\n\t}\n\tsecretLabels := map[string]string{}\n\tif d.config.SecretAnnotations != nil {\n\t\tfor k, v := range d.config.SecretAnnotations {\n\t\t\tsecretAnnotations[k] = v\n\t\t}\n\t}\n\tif d.config.SecretLabels != nil {\n\t\tfor k, v := range d.config.SecretLabels {\n\t\t\tsecretLabels[k] = v\n\t\t}\n\t}\n\n\t// 获取 Secret 实例，如果不存在则创建\n\tsecretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Get(ctx, d.config.SecretName, k8smeta.GetOptions{})\n\tif err != nil {\n\t\tif !k8serrors.IsNotFound(err) {\n\t\t\treturn nil, fmt.Errorf(\"failed to get kubernetes secret: %w\", err)\n\t\t}\n\n\t\tsecretPayload = &k8score.Secret{\n\t\t\tTypeMeta: k8smeta.TypeMeta{\n\t\t\t\tKind:       \"Secret\",\n\t\t\t\tAPIVersion: \"v1\",\n\t\t\t},\n\t\t\tObjectMeta: k8smeta.ObjectMeta{\n\t\t\t\tName:        d.config.SecretName,\n\t\t\t\tAnnotations: secretAnnotations,\n\t\t\t\tLabels:      secretLabels,\n\t\t\t},\n\t\t\tType: k8score.SecretType(d.config.SecretType),\n\t\t}\n\t\tsecretPayload.Data = make(map[string][]byte)\n\t\tsecretPayload.Data[d.config.SecretDataKeyForCrt] = []byte(certPEM)\n\t\tsecretPayload.Data[d.config.SecretDataKeyForKey] = []byte(privkeyPEM)\n\n\t\tsecretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Create(ctx, secretPayload, k8smeta.CreateOptions{})\n\t\td.logger.Debug(\"kubernetes operate 'Secrets.Create'\", slog.String(\"namespace\", d.config.Namespace), slog.Any(\"secret\", secretPayload))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create kubernetes secret: %w\", err)\n\t\t} else {\n\t\t\treturn &deployer.DeployResult{}, nil\n\t\t}\n\t}\n\n\t// 更新 Secret 实例\n\tsecretPayload.Type = k8score.SecretType(d.config.SecretType)\n\tif secretPayload.ObjectMeta.Annotations == nil {\n\t\tsecretPayload.ObjectMeta.Annotations = secretAnnotations\n\t} else {\n\t\tfor k, v := range secretAnnotations {\n\t\t\tsecretPayload.ObjectMeta.Annotations[k] = v\n\t\t}\n\t}\n\tif secretPayload.ObjectMeta.Labels == nil {\n\t\tsecretPayload.ObjectMeta.Labels = secretLabels\n\t} else {\n\t\tfor k, v := range secretLabels {\n\t\t\tsecretPayload.ObjectMeta.Labels[k] = v\n\t\t}\n\t}\n\tif secretPayload.Data == nil {\n\t\tsecretPayload.Data = make(map[string][]byte)\n\t}\n\tsecretPayload.Data[d.config.SecretDataKeyForCrt] = []byte(certPEM)\n\tsecretPayload.Data[d.config.SecretDataKeyForKey] = []byte(privkeyPEM)\n\tsecretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Update(ctx, secretPayload, k8smeta.UpdateOptions{})\n\td.logger.Debug(\"kubernetes operate 'Secrets.Update'\", slog.String(\"namespace\", d.config.Namespace), slog.Any(\"secret\", secretPayload))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to update kubernetes secret: %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createK8sClient(kubeConfig string) (*kubernetes.Clientset, error) {\n\tvar config *rest.Config\n\tvar err error\n\tif kubeConfig == \"\" {\n\t\tconfig, err = rest.InClusterConfig()\n\t} else {\n\t\tkubeConfig, err := clientcmd.NewClientConfigFromBytes([]byte(kubeConfig))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tconfig, err = kubeConfig.ClientConfig()\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient, err := kubernetes.NewForConfig(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/k8s-secret/k8s_secret_test.go",
    "content": "package k8ssecret_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/k8s-secret\"\n)\n\nvar (\n\tfInputCertPath       string\n\tfInputKeyPath        string\n\tfNamespace           string\n\tfSecretName          string\n\tfSecretDataKeyForCrt string\n\tfSecretDataKeyForKey string\n)\n\nfunc init() {\n\targsPrefix := \"K8SSECRET_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fNamespace, argsPrefix+\"NAMESPACE\", \"default\", \"\")\n\tflag.StringVar(&fSecretName, argsPrefix+\"SECRETNAME\", \"\", \"\")\n\tflag.StringVar(&fSecretDataKeyForCrt, argsPrefix+\"SECRETDATAKEYFORCRT\", \"tls.crt\", \"\")\n\tflag.StringVar(&fSecretDataKeyForKey, argsPrefix+\"SECRETDATAKEYFORKEY\", \"tls.key\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./k8s_secret_test.go -args \\\n\t--K8SSECRET_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--K8SSECRET_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--K8SSECRET_NAMESPACE=\"default\" \\\n\t--K8SSECRET_SECRETNAME=\"secret\" \\\n\t--K8SSECRET_SECRETDATAKEYFORCRT=\"tls.crt\" \\\n\t--K8SSECRET_SECRETDATAKEYFORKEY=\"tls.key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"NAMESPACE: %v\", fNamespace),\n\t\t\tfmt.Sprintf(\"SECRETNAME: %v\", fSecretName),\n\t\t\tfmt.Sprintf(\"SECRETDATAKEYFORCRT: %v\", fSecretDataKeyForCrt),\n\t\t\tfmt.Sprintf(\"SECRETDATAKEYFORKEY: %v\", fSecretDataKeyForKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tNamespace:           fNamespace,\n\t\t\tSecretName:          fSecretName,\n\t\t\tSecretDataKeyForCrt: fSecretDataKeyForCrt,\n\t\t\tSecretDataKeyForKey: fSecretDataKeyForKey,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/kong/consts.go",
    "content": "package kong\n\nconst (\n\t// 资源类型：替换指定证书。\n\tRESOURCE_TYPE_CERTIFICATE = \"certificate\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/kong/kong.go",
    "content": "package kong\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\n\t\"github.com/kong/go-kong/kong\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txhttp \"github.com/certimate-go/certimate/pkg/utils/http\"\n)\n\ntype DeployerConfig struct {\n\t// Kong 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// Kong Admin API Token。\n\tApiToken string `json:\"apiToken,omitempty\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 工作空间。\n\t// 选填。\n\tWorkspace string `json:\"workspace,omitempty\"`\n\t// 证书 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。\n\tCertificateId string `json:\"certificateId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *kong.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.Workspace, config.ApiToken, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_CERTIFICATE:\n\t\tif err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.CertificateId == \"\" {\n\t\treturn errors.New(\"config `certificateId` is required\")\n\t}\n\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 更新证书\n\t// REF: https://developer.konghq.com/api/gateway/admin-ee/3.10/#/operations/upsert-certificate\n\t// REF: https://developer.konghq.com/api/gateway/admin-ee/3.10/#/operations/upsert-certificate-in-workspace\n\tupdateCertificateReq := &kong.Certificate{\n\t\tID:   kong.String(d.config.CertificateId),\n\t\tCert: kong.String(certPEM),\n\t\tKey:  kong.String(privkeyPEM),\n\t\tSNIs: kong.StringSlice(certX509.DNSNames...),\n\t}\n\tupdateCertificateResp, err := d.sdkClient.Certificates.Update(ctx, updateCertificateReq)\n\td.logger.Debug(\"sdk request 'kong.UpdateCertificate'\", slog.String(\"sslId\", d.config.CertificateId), slog.Any(\"request\", updateCertificateReq), slog.Any(\"response\", updateCertificateResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'kong.UpdateCertificate': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(serverUrl, workspace, apiToken string, skipTlsVerify bool) (*kong.Client, error) {\n\thttpClient := &http.Client{\n\t\tTransport: xhttp.NewDefaultTransport(),\n\t\tTimeout:   http.DefaultClient.Timeout,\n\t}\n\tif skipTlsVerify {\n\t\ttransport := xhttp.NewDefaultTransport()\n\t\ttransport.DisableKeepAlives = true\n\t\ttransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}\n\t\thttpClient.Transport = transport\n\t} else {\n\t\thttpClient.Transport = http.DefaultTransport\n\t}\n\n\thttpHeaders := http.Header{}\n\tif apiToken != \"\" {\n\t\thttpHeaders.Set(\"Kong-Admin-Token\", apiToken)\n\t}\n\n\tclient, err := kong.NewClient(kong.String(serverUrl), kong.HTTPClientWithHeaders(httpClient, httpHeaders))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif workspace != \"\" {\n\t\tclient.SetWorkspace(workspace)\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/kong/kong_test.go",
    "content": "package kong_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/kong\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfApiToken      string\n\tfCertificateId string\n)\n\nfunc init() {\n\targsPrefix := \"KONG_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiToken, argsPrefix+\"APITOKEN\", \"\", \"\")\n\tflag.StringVar(&fCertificateId, argsPrefix+\"CERTIFICATEID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./kong_test.go -args \\\n\t--KONG_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--KONG_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--KONG_SERVERURL=\"http://127.0.0.1:9080\" \\\n\t--KONG_APITOKEN=\"your-admin-token\" \\\n\t--KONG_CERTIFICATEID=\"your-certificate-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APITOKEN: %v\", fApiToken),\n\t\t\tfmt.Sprintf(\"CERTIFICATEID: %v\", fCertificateId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiToken:                 fApiToken,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tResourceType:             provider.RESOURCE_TYPE_CERTIFICATE,\n\t\t\tCertificateId:            fCertificateId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ksyun-cdn/consts.go",
    "content": "package ksyuncdn\n\nconst (\n\t// 资源类型：替换指定域名的证书。\n\tRESOURCE_TYPE_DOMAIN = \"domain\"\n\t// 资源类型：替换指定证书。\n\tRESOURCE_TYPE_CERTIFICATE = \"certificate\"\n)\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/ksyun-cdn/ksyun_cdn.go",
    "content": "package ksyuncdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/KscSDK/ksc-sdk-go/ksc\"\n\tksccdnv1 \"github.com/KscSDK/ksc-sdk-go/service/cdnv1\"\n\t\"github.com/go-viper/mapstructure/v2\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 金山云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 金山云 SecretAccessKey。\n\tSecretAccessKey string `json:\"secretAccessKey\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n\t// 证书 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。\n\tCertificateId string `json:\"certificateId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *ksccdnv1.Cdnv1\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_DOMAIN:\n\t\tif err := d.deployToDomain(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_CERTIFICATE:\n\t\tif err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToDomain(ctx context.Context, certPEM, privkeyPEM string) error {\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain)\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no cdn domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found cdn domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, certPEM, privkeyPEM); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.CertificateId == \"\" {\n\t\treturn errors.New(\"config `certificateId` is required\")\n\t}\n\n\t// 更新证书\n\t// https://docs.ksyun.com/documents/259\n\tsetCertificateInput := map[string]any{\n\t\t\"CertificateId\":     d.config.CertificateId,\n\t\t\"CertificateName\":   fmt.Sprintf(\"certimate_%d\", time.Now().UnixMilli()),\n\t\t\"ServerCertificate\": certPEM,\n\t\t\"PrivateKey\":        privkeyPEM,\n\t}\n\tsetCertificateReq, setCertificateOutput := d.sdkClient.SetCertificatePostRequest(&setCertificateInput)\n\tsetCertificateErr := setCertificateReq.Send()\n\td.logger.Debug(\"sdk request 'cdn.SetCertificate'\", slog.Any(\"request\", setCertificateInput), slog.Any(\"response\", setCertificateOutput))\n\tif setCertificateErr != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.SetCertificate': %w\", setCertificateErr)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// https://docs.ksyun.com/documents/198\n\tgetCdnDomainsPageNumber := 1\n\tgetCdnDomainsPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tgetCdnDomainsInput := map[string]any{\n\t\t\t\"PageNumber\": getCdnDomainsPageNumber,\n\t\t\t\"PageSize\":   getCdnDomainsPageSize,\n\t\t}\n\t\tgetCdnDomainsReq, getCdnDomainsOutput := d.sdkClient.GetCdnDomainsPostRequest(&getCdnDomainsInput)\n\t\tgetCdnDomainsErr := getCdnDomainsReq.Send()\n\t\td.logger.Debug(\"sdk request 'cdn.GetCdnDomains'\", slog.Any(\"request\", getCdnDomainsInput), slog.Any(\"response\", getCdnDomainsOutput))\n\t\tif getCdnDomainsErr != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.GetCdnDomains': %w\", getCdnDomainsErr)\n\t\t}\n\n\t\ttype GetCdnDomainsResponse struct {\n\t\t\tPageNumber int32 `json:\"PageNumber\"`\n\t\t\tPageSize   int32 `json:\"PageSize\"`\n\t\t\tTotalCount int32 `json:\"TotalCount\"`\n\t\t\tDomains    []*struct {\n\t\t\t\tDomainId     string `json:\"DomainId\"`\n\t\t\t\tDomainName   string `json:\"DomainName\"`\n\t\t\t\tCname        string `json:\"Cname\"`\n\t\t\t\tCdnType      string `json:\"CdnType\"`\n\t\t\t\tCreatedTime  string `json:\"CreatedTime\"`\n\t\t\t\tModifiedTime string `json:\"ModifiedTime\"`\n\t\t\t\tRegion       string `json:\"Region\"`\n\t\t\t} `json:\"Domains\"`\n\t\t}\n\t\tvar getCdnDomainsResp *GetCdnDomainsResponse\n\t\tmapstructure.Decode(getCdnDomainsOutput, &getCdnDomainsResp)\n\t\tif getCdnDomainsResp == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, domainItem := range getCdnDomainsResp.Domains {\n\t\t\tdomains = append(domains, domainItem.DomainName)\n\t\t}\n\n\t\tif len(getCdnDomainsResp.Domains) < getCdnDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tgetCdnDomainsPageNumber++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) findDomainIdByDomain(ctx context.Context, domain string) (string, error) {\n\t// 查询域名列表\n\t// https://docs.ksyun.com/documents/198\n\tgetCdnDomainsPageNumber := 1\n\tgetCdnDomainsPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn \"\", ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tgetCdnDomainsInput := map[string]any{\n\t\t\t\"PageNumber\": getCdnDomainsPageNumber,\n\t\t\t\"PageSize\":   getCdnDomainsPageSize,\n\t\t\t\"DomainName\": domain,\n\t\t\t\"FuzzyMatch\": \"off\",\n\t\t}\n\t\tgetCdnDomainsReq, getCdnDomainsOutput := d.sdkClient.GetCdnDomainsPostRequest(&getCdnDomainsInput)\n\t\tgetCdnDomainsErr := getCdnDomainsReq.Send()\n\t\td.logger.Debug(\"sdk request 'cdn.GetCdnDomains'\", slog.Any(\"request\", getCdnDomainsInput), slog.Any(\"response\", getCdnDomainsOutput))\n\t\tif getCdnDomainsErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to execute sdk request 'cdn.GetCdnDomains': %w\", getCdnDomainsErr)\n\t\t}\n\n\t\ttype GetCdnDomainsResponse struct {\n\t\t\tPageNumber int32 `json:\"PageNumber\"`\n\t\t\tPageSize   int32 `json:\"PageSize\"`\n\t\t\tTotalCount int32 `json:\"TotalCount\"`\n\t\t\tDomains    []*struct {\n\t\t\t\tDomainId     string `json:\"DomainId\"`\n\t\t\t\tDomainName   string `json:\"DomainName\"`\n\t\t\t\tCname        string `json:\"Cname\"`\n\t\t\t\tCdnType      string `json:\"CdnType\"`\n\t\t\t\tCreatedTime  string `json:\"CreatedTime\"`\n\t\t\t\tModifiedTime string `json:\"ModifiedTime\"`\n\t\t\t\tRegion       string `json:\"Region\"`\n\t\t\t} `json:\"Domains\"`\n\t\t}\n\t\tvar getCdnDomainsResp *GetCdnDomainsResponse\n\t\tmapstructure.Decode(getCdnDomainsOutput, &getCdnDomainsResp)\n\t\tif getCdnDomainsResp == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, domainItem := range getCdnDomainsResp.Domains {\n\t\t\tif strings.EqualFold(domainItem.DomainName, domain) {\n\t\t\t\treturn domainItem.DomainId, nil\n\t\t\t}\n\t\t}\n\n\t\tif len(getCdnDomainsResp.Domains) < getCdnDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tgetCdnDomainsPageNumber++\n\t}\n\n\treturn \"\", fmt.Errorf(\"could not find domain '%s'\", domain)\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, certPEM, privkeyPEM string) error {\n\t// 获取域名 ID\n\tdomainId, err := d.findDomainIdByDomain(ctx, domain)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 为加速域名配置证书接口\n\t// https://docs.ksyun.com/documents/261\n\tconfigCertificateInput := map[string]any{\n\t\t\"Enable\":            \"on\",\n\t\t\"DomainIds\":         domainId,\n\t\t\"CertificateName\":   fmt.Sprintf(\"certimate_%d\", time.Now().UnixMilli()),\n\t\t\"ServerCertificate\": certPEM,\n\t\t\"PrivateKey\":        privkeyPEM,\n\t}\n\tconfigCertificateReq, configCertificateOutput := d.sdkClient.ConfigCertificatePostRequest(&configCertificateInput)\n\tconfigCertificateErr := configCertificateReq.Send()\n\td.logger.Debug(\"sdk request 'cdn.ConfigCertificate'\", slog.Any(\"request\", configCertificateInput), slog.Any(\"response\", configCertificateOutput))\n\tif configCertificateErr != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.ConfigCertificate': %w\", configCertificateErr)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, secretAccessKey string) (*ksccdnv1.Cdnv1, error) {\n\tregion := \"cn-beijing-6\"\n\tclient := ksccdnv1.SdkNew(ksc.NewClient(accessKeyId, secretAccessKey), &ksc.Config{Region: &region})\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ksyun-cdn/ksyun_cdn_test.go",
    "content": "package ksyuncdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ksyun-cdn\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfSecretAccessKey string\n\tfDomain          string\n\tfCertificateId   string\n)\n\nfunc init() {\n\targsPrefix := \"KSYUNCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fSecretAccessKey, argsPrefix+\"SECRETACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n\tflag.StringVar(&fCertificateId, argsPrefix+\"CERTIFICATEID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ksyun_cdn_test.go -args \\\n\t--KSYUNCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--KSYUNCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--KSYUNCDN_ACCESSKEYID=\"your-access-key-id\" \\\n\t--KSYUNCDN_SECRETACCESSKEY=\"your-secret-access-key\" \\\n\t--KSYUNCDN_DOMAIN=\"example.com\" \\\n\t--KSYUNCDN_CERTIFICATEID=\"your-certificate-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"SECRETACCESSKEY: %v\", fSecretAccessKey),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t\tfmt.Sprintf(\"CERTIFICATEID: %v\", fCertificateId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tSecretAccessKey:    fSecretAccessKey,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t\tCertificateId:      fCertificateId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/lecdn/consts.go",
    "content": "package lecdn\n\nconst (\n\t// 资源类型：替换指定证书。\n\tRESOURCE_TYPE_CERTIFICATE = \"certificate\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/lecdn/lecdn.go",
    "content": "package lecdn\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tleclientsdkv3 \"github.com/certimate-go/certimate/pkg/sdk3rd/lecdn/v3/client\"\n\tlemastersdkv3 \"github.com/certimate-go/certimate/pkg/sdk3rd/lecdn/v3/master\"\n)\n\ntype DeployerConfig struct {\n\t// LeCDN 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// LeCDN 版本。\n\t// 可取值 \"v3\"。\n\tApiVersion string `json:\"apiVersion\"`\n\t// LeCDN 用户角色。\n\t// 可取值 \"client\"、\"master\"。\n\tApiRole string `json:\"apiRole\"`\n\t// LeCDN 用户名。\n\tUsername string `json:\"username\"`\n\t// LeCDN 用户密码。\n\tPassword string `json:\"password\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 证书 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。\n\tCertificateId int64 `json:\"certificateId,omitempty\"`\n\t// 客户 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时选填。\n\tClientId int64 `json:\"clientId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient any\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiVersion, config.ApiRole, config.Username, config.Password, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_CERTIFICATE:\n\t\tif err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.CertificateId == 0 {\n\t\treturn errors.New(\"config `certificateId` is required\")\n\t}\n\n\t// 修改证书\n\t// REF: https://wdk0pwf8ul.feishu.cn/wiki/YE1XwCRIHiLYeKkPupgcXrlgnDd\n\tswitch sdkClient := d.sdkClient.(type) {\n\tcase *leclientsdkv3.Client:\n\t\t{\n\t\t\tupdateSSLCertReq := &leclientsdkv3.UpdateCertificateRequest{\n\t\t\t\tName:        fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli()),\n\t\t\t\tDescription: \"upload from certimate\",\n\t\t\t\tType:        \"upload\",\n\t\t\t\tSSLPEM:      certPEM,\n\t\t\t\tSSLKey:      privkeyPEM,\n\t\t\t\tAutoRenewal: false,\n\t\t\t}\n\t\t\tupdateSSLCertResp, err := sdkClient.UpdateCertificateWithContext(ctx, d.config.CertificateId, updateSSLCertReq)\n\t\t\td.logger.Debug(\"sdk request 'lecdn.UpdateCertificate'\", slog.Int64(\"certId\", d.config.CertificateId), slog.Any(\"request\", updateSSLCertReq), slog.Any(\"response\", updateSSLCertResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'lecdn.UpdateCertificate': %w\", err)\n\t\t\t}\n\t\t}\n\n\tcase *lemastersdkv3.Client:\n\t\t{\n\t\t\tupdateSSLCertReq := &lemastersdkv3.UpdateCertificateRequest{\n\t\t\t\tClientId:    d.config.ClientId,\n\t\t\t\tName:        fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli()),\n\t\t\t\tDescription: \"upload from certimate\",\n\t\t\t\tType:        \"upload\",\n\t\t\t\tSSLPEM:      certPEM,\n\t\t\t\tSSLKey:      privkeyPEM,\n\t\t\t\tAutoRenewal: false,\n\t\t\t}\n\t\t\tupdateSSLCertResp, err := sdkClient.UpdateCertificateWithContext(ctx, d.config.CertificateId, updateSSLCertReq)\n\t\t\td.logger.Debug(\"sdk request 'lecdn.UpdateCertificate'\", slog.Int64(\"certId\", d.config.CertificateId), slog.Any(\"request\", updateSSLCertReq), slog.Any(\"response\", updateSSLCertResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'lecdn.UpdateCertificate': %w\", err)\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\tpanic(\"unreachable\")\n\t}\n\n\treturn nil\n}\n\nconst (\n\tsdkVersionV3 = \"v3\"\n\n\tsdkRoleClient = \"client\"\n\tsdkRoleMaster = \"master\"\n)\n\nfunc createSDKClient(serverUrl, apiVersion, apiRole, username, password string, skipTlsVerify bool) (any, error) {\n\tif apiVersion == sdkVersionV3 && apiRole == sdkRoleClient {\n\t\t// v3 版客户端\n\t\tclient, err := leclientsdkv3.NewClient(serverUrl, username, password)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif skipTlsVerify {\n\t\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t\t}\n\n\t\treturn client, nil\n\t} else if apiVersion == sdkVersionV3 && apiRole == sdkRoleMaster {\n\t\t// v3 版主控端\n\t\tclient, err := lemastersdkv3.NewClient(serverUrl, username, password)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif skipTlsVerify {\n\t\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t\t}\n\n\t\treturn client, nil\n\t}\n\n\treturn nil, errors.New(\"lecdn: invalid api version or user role\")\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/lecdn/lecdn_test.go",
    "content": "package lecdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/lecdn\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfApiVersion    string\n\tfUsername      string\n\tfPassword      string\n\tfCertificateId int64\n)\n\nfunc init() {\n\targsPrefix := \"LECDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiVersion, argsPrefix+\"APIVERSION\", \"v3\", \"\")\n\tflag.StringVar(&fUsername, argsPrefix+\"USERNAME\", \"\", \"\")\n\tflag.StringVar(&fPassword, argsPrefix+\"PASSWORD\", \"\", \"\")\n\tflag.Int64Var(&fCertificateId, argsPrefix+\"CERTIFICATEID\", 0, \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./lecdn_test.go -args \\\n\t--LECDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--LECDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--LECDN_SERVERURL=\"http://127.0.0.1:5090\" \\\n\t--LECDN_USERNAME=\"your-username\" \\\n\t--LECDN_PASSWORD=\"your-password\" \\\n\t--LECDN_CERTIFICATEID=\"your-certificate-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy_ToCertificate\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APIVERSION: %v\", fApiVersion),\n\t\t\tfmt.Sprintf(\"USERNAME: %v\", fUsername),\n\t\t\tfmt.Sprintf(\"PASSWORD: %v\", fPassword),\n\t\t\tfmt.Sprintf(\"CERTIFICATEID: %v\", fCertificateId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiVersion:               fApiVersion,\n\t\t\tApiRole:                  \"user\",\n\t\t\tUsername:                 fUsername,\n\t\t\tPassword:                 fPassword,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tResourceType:             provider.RESOURCE_TYPE_CERTIFICATE,\n\t\t\tCertificateId:            fCertificateId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/local/consts.go",
    "content": "package local\n\nimport (\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\nconst (\n\tSHELL_ENV_SH         = \"sh\"\n\tSHELL_ENV_CMD        = \"cmd\"\n\tSHELL_ENV_POWERSHELL = \"powershell\"\n)\n\nconst (\n\tOUTPUT_FORMAT_PEM = string(domain.CertificateFormatTypePEM)\n\tOUTPUT_FORMAT_PFX = string(domain.CertificateFormatTypePFX)\n\tOUTPUT_FORMAT_JKS = string(domain.CertificateFormatTypeJKS)\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/local/local.go",
    "content": "package local\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txfile \"github.com/certimate-go/certimate/pkg/utils/file\"\n)\n\ntype DeployerConfig struct {\n\t// Shell 执行环境。\n\t// 零值时根据操作系统决定。\n\tShellEnv string `json:\"shellEnv,omitempty\"`\n\t// 前置命令。\n\tPreCommand string `json:\"preCommand,omitempty\"`\n\t// 后置命令。\n\tPostCommand string `json:\"postCommand,omitempty\"`\n\t// 输出证书格式。\n\tOutputFormat string `json:\"outputFormat,omitempty\"`\n\t// 输出证书文件路径。\n\tOutputCertPath string `json:\"outputCertPath,omitempty\"`\n\t// 输出服务器证书文件路径。\n\t// 选填。\n\tOutputServerCertPath string `json:\"outputServerCertPath,omitempty\"`\n\t// 输出中间证书文件路径。\n\t// 选填。\n\tOutputIntermediaCertPath string `json:\"outputIntermediaCertPath,omitempty\"`\n\t// 输出私钥文件路径。\n\tOutputKeyPath string `json:\"outputKeyPath,omitempty\"`\n\t// PFX 导出密码。\n\t// 证书格式为 PFX 时必填。\n\tPfxPassword string `json:\"pfxPassword,omitempty\"`\n\t// JKS 别名。\n\t// 证书格式为 JKS 时必填。\n\tJksAlias string `json:\"jksAlias,omitempty\"`\n\t// JKS 密钥密码。\n\t// 证书格式为 JKS 时必填。\n\tJksKeypass string `json:\"jksKeypass,omitempty\"`\n\t// JKS 存储密码。\n\t// 证书格式为 JKS 时必填。\n\tJksStorepass string `json:\"jksStorepass,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig *DeployerConfig\n\tlogger *slog.Logger\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\treturn &Deployer{\n\t\tconfig: config,\n\t\tlogger: slog.Default(),\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 提取服务器证书和中间证书\n\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to extract certs: %w\", err)\n\t}\n\n\t// 执行前置命令\n\tif d.config.PreCommand != \"\" {\n\t\tcommand := d.config.PreCommand\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}\", d.config.OutputCertPath)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}\", d.config.OutputServerCertPath)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_INTERMEDIA_PATH}\", d.config.OutputIntermediaCertPath)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}\", d.config.OutputKeyPath)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}\", d.config.PfxPassword)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_JKS_ALIAS}\", d.config.JksAlias)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_JKS_KEYPASS}\", d.config.JksKeypass)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_JKS_STOREPASS}\", d.config.JksStorepass)\n\n\t\tstdout, stderr, err := execCommand(d.config.ShellEnv, command)\n\t\td.logger.Debug(\"run pre-command\", slog.String(\"stdout\", stdout), slog.String(\"stderr\", stderr))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute pre-command (stdout: %s, stderr: %s): %w \", stdout, stderr, err)\n\t\t}\n\t}\n\n\t// 写入证书和私钥文件\n\tswitch d.config.OutputFormat {\n\tcase OUTPUT_FORMAT_PEM:\n\t\t{\n\t\t\tif err := xfile.WriteString(d.config.OutputCertPath, certPEM); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to save certificate file: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl certificate file saved\", slog.String(\"path\", d.config.OutputCertPath))\n\n\t\t\tif d.config.OutputServerCertPath != \"\" {\n\t\t\t\tif err := xfile.WriteString(d.config.OutputServerCertPath, serverCertPEM); err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to save server certificate file: %w\", err)\n\t\t\t\t}\n\t\t\t\td.logger.Info(\"ssl server certificate file saved\", slog.String(\"path\", d.config.OutputServerCertPath))\n\t\t\t}\n\n\t\t\tif d.config.OutputIntermediaCertPath != \"\" {\n\t\t\t\tif err := xfile.WriteString(d.config.OutputIntermediaCertPath, intermediaCertPEM); err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to save intermedia certificate file: %w\", err)\n\t\t\t\t}\n\t\t\t\td.logger.Info(\"ssl intermedia certificate file saved\", slog.String(\"path\", d.config.OutputIntermediaCertPath))\n\t\t\t}\n\n\t\t\tif err := xfile.WriteString(d.config.OutputKeyPath, privkeyPEM); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to save private key file: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl private key file saved\", slog.String(\"path\", d.config.OutputKeyPath))\n\t\t}\n\n\tcase OUTPUT_FORMAT_PFX:\n\t\t{\n\t\t\tpfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to transform certificate to PFX: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl certificate transformed to pfx\")\n\n\t\t\tif err := xfile.Write(d.config.OutputCertPath, pfxData); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to save certificate file: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl certificate file saved\", slog.String(\"path\", d.config.OutputCertPath))\n\t\t}\n\n\tcase OUTPUT_FORMAT_JKS:\n\t\t{\n\t\t\tjksData, err := xcert.TransformCertificateFromPEMToJKS(certPEM, privkeyPEM, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to transform certificate to JKS: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl certificate transformed to jks\")\n\n\t\t\tif err := xfile.Write(d.config.OutputCertPath, jksData); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to save certificate file: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl certificate file saved\", slog.String(\"path\", d.config.OutputCertPath))\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported output format '%s'\", d.config.OutputFormat)\n\t}\n\n\t// 执行后置命令\n\tif d.config.PostCommand != \"\" {\n\t\tcommand := d.config.PostCommand\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}\", d.config.OutputCertPath)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}\", d.config.OutputServerCertPath)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_INTERMEDIA_PATH}\", d.config.OutputIntermediaCertPath)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}\", d.config.OutputKeyPath)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}\", d.config.PfxPassword)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_JKS_ALIAS}\", d.config.JksAlias)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_JKS_KEYPASS}\", d.config.JksKeypass)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_JKS_STOREPASS}\", d.config.JksStorepass)\n\n\t\tstdout, stderr, err := execCommand(d.config.ShellEnv, command)\n\t\td.logger.Debug(\"run post-command\", slog.String(\"stdout\", stdout), slog.String(\"stderr\", stderr))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute post-command (stdout: %s, stderr: %s): %w \", stdout, stderr, err)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc execCommand(shellEnv string, command string) (string, string, error) {\n\tvar cmd *exec.Cmd\n\n\tswitch shellEnv {\n\tcase \"\":\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tcmd = exec.Command(\"cmd\", \"/C\", command)\n\t\t} else {\n\t\t\tcmd = exec.Command(\"sh\", \"-c\", command)\n\t\t}\n\n\tcase SHELL_ENV_SH:\n\t\tcmd = exec.Command(\"sh\", \"-c\", command)\n\n\tcase SHELL_ENV_CMD:\n\t\tcmd = exec.Command(\"cmd\", \"/C\", command)\n\n\tcase SHELL_ENV_POWERSHELL:\n\t\tcmd = exec.Command(\"powershell\", \"-Command\", command)\n\n\tdefault:\n\t\treturn \"\", \"\", fmt.Errorf(\"unsupported shell env '%s'\", shellEnv)\n\t}\n\n\tstdoutBuf := bytes.NewBuffer(nil)\n\tcmd.Stdout = stdoutBuf\n\tstderrBuf := bytes.NewBuffer(nil)\n\tcmd.Stderr = stderrBuf\n\terr := cmd.Run()\n\tif err != nil {\n\t\treturn stdoutBuf.String(), stderrBuf.String(), fmt.Errorf(\"failed to execute command: %w\", err)\n\t}\n\n\treturn stdoutBuf.String(), stderrBuf.String(), nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/local/local_test.go",
    "content": "package local_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/local\"\n)\n\nvar (\n\tfInputCertPath  string\n\tfInputKeyPath   string\n\tfOutputCertPath string\n\tfOutputKeyPath  string\n\tfPfxPassword    string\n\tfJksAlias       string\n\tfJksKeypass     string\n\tfJksStorepass   string\n\tfShellEnv       string\n\tfPreCommand     string\n\tfPostCommand    string\n)\n\nfunc init() {\n\targsPrefix := \"LOCAL_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fOutputCertPath, argsPrefix+\"OUTPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fOutputKeyPath, argsPrefix+\"OUTPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fPfxPassword, argsPrefix+\"PFXPASSWORD\", \"\", \"\")\n\tflag.StringVar(&fJksAlias, argsPrefix+\"JKSALIAS\", \"\", \"\")\n\tflag.StringVar(&fJksKeypass, argsPrefix+\"JKSKEYPASS\", \"\", \"\")\n\tflag.StringVar(&fJksStorepass, argsPrefix+\"JKSSTOREPASS\", \"\", \"\")\n\tflag.StringVar(&fShellEnv, argsPrefix+\"SHELLENV\", \"\", \"\")\n\tflag.StringVar(&fPreCommand, argsPrefix+\"PRECOMMAND\", \"\", \"\")\n\tflag.StringVar(&fPostCommand, argsPrefix+\"POSTCOMMAND\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./local_test.go -args \\\n\t--LOCAL_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--LOCAL_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--LOCAL_OUTPUTCERTPATH=\"/path/to/your-output-cert\" \\\n\t--LOCAL_OUTPUTKEYPATH=\"/path/to/your-output-key\" \\\n\t--LOCAL_PFXPASSWORD=\"your-pfx-password\" \\\n\t--LOCAL_JKSALIAS=\"your-jks-alias\" \\\n\t--LOCAL_JKSKEYPASS=\"your-jks-keypass\" \\\n\t--LOCAL_JKSSTOREPASS=\"your-jks-storepass\" \\\n\t--LOCAL_SHELLENV=\"sh\" \\\n\t--LOCAL_PRECOMMAND=\"echo 'hello world'\" \\\n\t--LOCAL_POSTCOMMAND=\"echo 'bye-bye world'\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy_PEM\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"OUTPUTCERTPATH: %v\", fOutputCertPath),\n\t\t\tfmt.Sprintf(\"OUTPUTKEYPATH: %v\", fOutputKeyPath),\n\t\t\tfmt.Sprintf(\"SHELLENV: %v\", fShellEnv),\n\t\t\tfmt.Sprintf(\"PRECOMMAND: %v\", fPreCommand),\n\t\t\tfmt.Sprintf(\"POSTCOMMAND: %v\", fPostCommand),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tOutputFormat:   provider.OUTPUT_FORMAT_PEM,\n\t\t\tOutputCertPath: fOutputCertPath + \".pem\",\n\t\t\tOutputKeyPath:  fOutputKeyPath + \".pem\",\n\t\t\tShellEnv:       fShellEnv,\n\t\t\tPreCommand:     fPreCommand,\n\t\t\tPostCommand:    fPostCommand,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfstat1, err := os.Stat(fOutputCertPath + \".pem\")\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t} else if fstat1.Size() == 0 {\n\t\t\tt.Errorf(\"err: empty output certificate file\")\n\t\t\treturn\n\t\t}\n\n\t\tfstat2, err := os.Stat(fOutputKeyPath + \".pem\")\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t} else if fstat2.Size() == 0 {\n\t\t\tt.Errorf(\"err: empty output private key file\")\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n\n\tt.Run(\"Deploy_PFX\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"OUTPUTCERTPATH: %v\", fOutputCertPath),\n\t\t\tfmt.Sprintf(\"PFXPASSWORD: %v\", fPfxPassword),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tOutputFormat:   provider.OUTPUT_FORMAT_PFX,\n\t\t\tOutputCertPath: fOutputCertPath + \".pfx\",\n\t\t\tPfxPassword:    fPfxPassword,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfstat, err := os.Stat(fOutputCertPath + \".pfx\")\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t} else if fstat.Size() == 0 {\n\t\t\tt.Errorf(\"err: empty output certificate file\")\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n\n\tt.Run(\"Deploy_JKS\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"OUTPUTCERTPATH: %v\", fOutputCertPath),\n\t\t\tfmt.Sprintf(\"JKSALIAS: %v\", fJksAlias),\n\t\t\tfmt.Sprintf(\"JKSKEYPASS: %v\", fJksKeypass),\n\t\t\tfmt.Sprintf(\"JKSSTOREPASS: %v\", fJksStorepass),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tOutputFormat:   provider.OUTPUT_FORMAT_JKS,\n\t\t\tOutputCertPath: fOutputCertPath + \".jks\",\n\t\t\tJksAlias:       fJksAlias,\n\t\t\tJksKeypass:     fJksKeypass,\n\t\t\tJksStorepass:   fJksStorepass,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfstat, err := os.Stat(fOutputCertPath + \".jks\")\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t} else if fstat.Size() == 0 {\n\t\t\tt.Errorf(\"err: empty output certificate file\")\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/mohua-mvh/mohua_mvh.go",
    "content": "package mohuamvh\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\n\tmohuasdk \"github.com/mohuatech/mohuacloud-go-sdk\"\n\tmohuasdktypes \"github.com/mohuatech/mohuacloud-go-sdk/types\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// 嘿华云账号。\n\tUsername string `json:\"username\"`\n\t// 嘿华云 API 密钥。\n\tApiPassword string `json:\"apiPassword\"`\n\t// 虚拟主机 ID。\n\tHostId string `json:\"hostId\"`\n\t// 域名 ID。\n\tDomainId string `json:\"domainId\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *mohuasdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.Username, config.ApiPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.HostId == \"\" {\n\t\treturn nil, errors.New(\"config `hostId` is required\")\n\t}\n\tif d.config.DomainId == \"\" {\n\t\treturn nil, errors.New(\"config `domainId` is required\")\n\t}\n\n\tdomainId, err := strconv.ParseInt(d.config.DomainId, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 登录获取 Token\n\t_, err = d.sdkClient.Auth.Login(\"\", \"\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to login mohua: %w\", err)\n\t}\n\n\t// 设置 SSL 证书\n\tsetSSLReq := &mohuasdktypes.SetSSLRequest{\n\t\tID:      int(domainId),\n\t\tSSLCert: certPEM,\n\t\tSSLKey:  privkeyPEM,\n\t}\n\tsetSSLResp, err := d.sdkClient.VirtualHost.SetSSL(d.config.HostId, setSSLReq)\n\td.logger.Debug(\"sdk request 'mvh.SetSSL'\", slog.Any(\"request\", setSSLReq), slog.Any(\"response\", setSSLResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'mvh.SetSSL': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(username, apiPassword string) (*mohuasdk.Client, error) {\n\tif username == \"\" {\n\t\treturn nil, errors.New(\"mohua: invalid username\")\n\t}\n\tif apiPassword == \"\" {\n\t\treturn nil, errors.New(\"mohua: invalid api password\")\n\t}\n\n\tclient := mohuasdk.NewClient(\n\t\tmohuasdk.WithCredentials(username, apiPassword),\n\t)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/mohua-mvh/mohua_mvh_test.go",
    "content": "package mohuamvh_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/mohua-mvh\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfUsername      string\n\tfApiPassword   string\n\tfHostID        string\n\tfDomainID      string\n)\n\nfunc init() {\n\targsPrefix := \"MOHUAMVH_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fUsername, argsPrefix+\"USERNAME\", \"\", \"\")\n\tflag.StringVar(&fApiPassword, argsPrefix+\"APIPASSWORD\", \"\", \"\")\n\tflag.StringVar(&fHostID, argsPrefix+\"HOSTID\", \"\", \"\")\n\tflag.StringVar(&fDomainID, argsPrefix+\"DOMAINID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./mohuamvh_test.go -args \\\n\t--MOHUAMVH_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--MOHUAMVH_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--MOHUAMVH_USERNAME=\"your-username\" \\\n\t--MOHUAMVH_APIPASSWORD=\"your-api-password\" \\\n\t--MOHUAMVH_HOSTID=\"your-virtual-host-id\" \\\n\t--MOHUAMVH_DOMAINID=\"your-domain-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"USERNAME: %v\", fUsername),\n\t\t\tfmt.Sprintf(\"APIPASSWORD: %v\", fApiPassword),\n\t\t\tfmt.Sprintf(\"HOSTID: %v\", fHostID),\n\t\t\tfmt.Sprintf(\"DOMAINID: %v\", fDomainID),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tUsername:    fUsername,\n\t\t\tApiPassword: fApiPassword,\n\t\t\tHostId:      fHostID,\n\t\t\tDomainId:    fDomainID,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/netlify/consts.go",
    "content": "package netlify\n\nconst (\n\t// 资源类型：替换指定网站的证书。\n\tRESOURCE_TYPE_WEBSITE = \"website\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/netlify/netlify.go",
    "content": "package netlify\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tnetlifysdk \"github.com/certimate-go/certimate/pkg/sdk3rd/netlify\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype DeployerConfig struct {\n\t// netlify API Token。\n\tApiToken string `json:\"apiToken\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// netlify 网站 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_WEBSITE] 时必填。\n\tSiteId string `json:\"siteId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *netlifysdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ApiToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_WEBSITE:\n\t\tif err := d.deployToWebsite(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToWebsite(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.SiteId == \"\" {\n\t\treturn errors.New(\"config `siteId` is required\")\n\t}\n\n\t// 提取服务器证书和中间证书\n\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to extract certs: %w\", err)\n\t}\n\n\t// 上传网站证书\n\t// REF: https://open-api.netlify.com/#tag/sniCertificate/operation/provisionSiteTLSCertificate\n\tprovisionSiteTLSCertificateReq := &netlifysdk.ProvisionSiteTLSCertificateParams{\n\t\tCertificate:    serverCertPEM,\n\t\tCACertificates: intermediaCertPEM,\n\t\tKey:            privkeyPEM,\n\t}\n\tprovisionSiteTLSCertificateResp, err := d.sdkClient.ProvisionSiteTLSCertificateWithContext(ctx, d.config.SiteId, provisionSiteTLSCertificateReq)\n\td.logger.Debug(\"sdk request 'netlify.provisionSiteTLSCertificate'\", slog.String(\"siteId\", d.config.SiteId), slog.Any(\"request\", provisionSiteTLSCertificateReq), slog.Any(\"response\", provisionSiteTLSCertificateResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'netlify.provisionSiteTLSCertificate': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(apiToken string) (*netlifysdk.Client, error) {\n\treturn netlifysdk.NewClient(apiToken)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/netlify/netlify_test.go",
    "content": "package netlify_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/netlify\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfApiToken      string\n\tfSiteId        string\n)\n\nfunc init() {\n\targsPrefix := \"NETLIFY_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fApiToken, argsPrefix+\"APITOKEN\", \"\", \"\")\n\tflag.StringVar(&fSiteId, argsPrefix+\"SITEID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./netlify_test.go -args \\\n\t--NETLIFY_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--NETLIFY_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--NETLIFY_APITOKEN=\"your-api-token\" \\\n\t--NETLIFY_SITEID=\"your-site-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"APITOKEN: %v\", fApiToken),\n\t\t\tfmt.Sprintf(\"SITEID: %v\", fSiteId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tApiToken:     fApiToken,\n\t\t\tResourceType: provider.RESOURCE_TYPE_WEBSITE,\n\t\t\tSiteId:       fSiteId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/nginxproxymanager/consts.go",
    "content": "package nginxproxymanager\n\nconst (\n\tAUTH_METHOD_PASSWORD = \"password\"\n\tAUTH_METHOD_TOKEN    = \"token\"\n)\n\nconst (\n\t// 资源类型：替换指定主机的证书。\n\tRESOURCE_TYPE_HOST = \"host\"\n\t// 资源类型：替换指定证书。\n\tRESOURCE_TYPE_CERTIFICATE = \"certificate\"\n)\n\nconst (\n\t// 匹配模式：指定 ID。\n\tHOST_MATCH_PATTERN_SPECIFIED = \"specified\"\n\t// 匹配模式：证书 SAN 匹配。\n\tHOST_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n\nconst (\n\tHOST_TYPE_PROXY       = \"proxy\"\n\tHOST_TYPE_REDIRECTION = \"redirection\"\n\tHOST_TYPE_STREAM      = \"stream\"\n\tHOST_TYPE_DEAD        = \"dead\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/nginxproxymanager/nginxproxymanager.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/nginxproxymanager\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tnpmsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/nginxproxymanager\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txwait \"github.com/certimate-go/certimate/pkg/utils/wait\"\n)\n\ntype DeployerConfig struct {\n\t// NPM 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// NPM API 认证方式。\n\t// 可取值 \"password\"、\"token\"。\n\t// 零值时默认值 [AUTH_METHOD_PASSWORD]。\n\tAuthMethod string `json:\"authMethod,omitempty\"`\n\t// NPM 用户名。\n\tUsername string `json:\"username,omitempty\"`\n\t// NPM 密码。\n\tPassword string `json:\"password,omitempty\"`\n\t// NPM API Token。\n\tApiToken string `json:\"apiToken,omitempty\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [HOST_MATCH_PATTERN_SPECIFIED]。\n\tHostMatchPattern string `json:\"hostMatchPattern,omitempty\"`\n\t// 主机类型。\n\t// 部署资源类型为 [RESOURCE_TYPE_HOST] 时必填。\n\tHostType string `json:\"hostType,omitempty\"`\n\t// 主机 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_HOST]、且匹配模式非 [HOST_MATCH_PATTERN_CERTSAN] 时必填。\n\tHostId int64 `json:\"hostId,omitempty\"`\n\t// 证书 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。\n\tCertificateId int64 `json:\"certificateId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *npmsdk.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.AuthMethod, config.Username, config.Password, config.ApiToken, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tServerUrl:                config.ServerUrl,\n\t\tAuthMethod:               config.AuthMethod,\n\t\tUsername:                 config.Username,\n\t\tPassword:                 config.Password,\n\t\tApiToken:                 config.ApiToken,\n\t\tAllowInsecureConnections: config.AllowInsecureConnections,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_HOST:\n\t\tif err := d.deployToHost(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_CERTIFICATE:\n\t\tif err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToHost(ctx context.Context, certPEM, privkeyPEM string) error {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的主机列表\n\tvar hostIds []int64\n\tswitch d.config.HostMatchPattern {\n\tcase \"\", HOST_MATCH_PATTERN_SPECIFIED:\n\t\t{\n\t\t\tif d.config.HostId == 0 {\n\t\t\t\treturn errors.New(\"config `hostId` is required\")\n\t\t\t}\n\n\t\t\thostIds = []int64{d.config.HostId}\n\t\t}\n\n\tcase HOST_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\thostCandidates, err := d.getAllHosts(ctx, d.config.HostType)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\thostIds = lo.Map(\n\t\t\t\tlo.Filter(hostCandidates, func(hostItem *npmsdk.HostRecord, _ int) bool {\n\t\t\t\t\treturn len(hostItem.DomainNames) > 0 &&\n\t\t\t\t\t\tlo.EveryBy(hostItem.DomainNames, func(domain string) bool {\n\t\t\t\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t\t\t\t})\n\t\t\t\t}),\n\t\t\t\tfunc(hostItem *npmsdk.HostRecord, _ int) int64 {\n\t\t\t\t\treturn hostItem.Id\n\t\t\t\t},\n\t\t\t)\n\t\t\tif len(hostIds) == 0 {\n\t\t\t\treturn errors.New(\"could not find any hosts matched by certificate\")\n\t\t\t}\n\n\t\t\t// 跳过已部署过的主机\n\t\t\thostIds = lo.Filter(hostIds, func(hostId int64, _ int) bool {\n\t\t\t\thostInfo, _ := lo.Find(hostCandidates, func(hostItem *npmsdk.HostRecord) bool {\n\t\t\t\t\treturn hostId == hostItem.Id\n\t\t\t\t})\n\t\t\t\tif hostInfo != nil {\n\t\t\t\t\treturn strconv.FormatInt(hostInfo.CertificateId, 10) != upres.CertId\n\t\t\t\t}\n\n\t\t\t\treturn true\n\t\t\t})\n\t\t}\n\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported host match pattern: '%s'\", d.config.HostMatchPattern)\n\t}\n\n\t// 遍历更新主机证书\n\tif len(hostIds) == 0 {\n\t\td.logger.Info(\"no hosts to deploy\")\n\t} else {\n\t\td.logger.Info(\"found hosts to deploy\", slog.Any(\"hostIds\", hostIds))\n\t\tvar errs []error\n\n\t\tcertId, _ := strconv.ParseInt(upres.CertId, 10, 64)\n\t\tfor i, hostId := range hostIds {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateHostCertificate(ctx, d.config.HostType, hostId, certId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t\tif i < len(hostIds)-1 {\n\t\t\t\t\txwait.DelayWithContext(ctx, time.Second*5)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.CertificateId == 0 {\n\t\treturn errors.New(\"config `certificateId` is required\")\n\t}\n\n\t// 替换证书\n\topres, err := d.sdkCertmgr.Replace(ctx, strconv.FormatInt(d.config.CertificateId, 10), certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to replace certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate replaced\", slog.Any(\"result\", opres))\n\t}\n\n\t// 获取默认站点\n\tsettingsGetDefaultSiteReq := &npmsdk.SettingsGetDefaultSiteRequest{}\n\tsettingsGetDefaultSiteResp, err := d.sdkClient.SettingsGetDefaultSiteWithContext(ctx, settingsGetDefaultSiteReq)\n\td.logger.Debug(\"sdk request 'settings.GetDefaultSite'\", slog.Any(\"request\", settingsGetDefaultSiteReq), slog.Any(\"response\", settingsGetDefaultSiteResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'settings.GetDefaultSite': %w\", err)\n\t}\n\n\t// 更新默认站点，以触发 nginx 重启\n\tsettingsSetDefaultSiteReq := &npmsdk.SettingsSetDefaultSiteRequest{\n\t\tValue: settingsGetDefaultSiteResp.Value,\n\t}\n\tsettingsSetDefaultSiteResp, err := d.sdkClient.SettingsSetDefaultSiteWithContext(ctx, settingsSetDefaultSiteReq)\n\td.logger.Debug(\"sdk request 'settings.SetDefaultSite'\", slog.Any(\"request\", settingsSetDefaultSiteReq), slog.Any(\"response\", settingsSetDefaultSiteResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'settings.SetDefaultSite': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) getAllHosts(ctx context.Context, cloudHostType string) ([]*npmsdk.HostRecord, error) {\n\tvar hosts []*npmsdk.HostRecord\n\tswitch cloudHostType {\n\tcase HOST_TYPE_PROXY:\n\t\t{\n\t\t\tnginxListProxyHostsReq := &npmsdk.NginxListProxyHostsRequest{}\n\t\t\tnginxListProxyHostsResp, err := d.sdkClient.NginxListProxyHostsWithContext(ctx, nginxListProxyHostsReq)\n\t\t\td.logger.Debug(\"sdk request 'nginx.ListProxyHosts'\", slog.Any(\"request\", nginxListProxyHostsReq), slog.Any(\"response\", nginxListProxyHostsResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'nginx.ListProxyHosts': %w\", err)\n\t\t\t}\n\n\t\t\thosts = make([]*npmsdk.HostRecord, 0, len(*nginxListProxyHostsResp))\n\t\t\tfor _, hostItem := range *nginxListProxyHostsResp {\n\t\t\t\thosts = append(hosts, &hostItem.HostRecord)\n\t\t\t}\n\t\t}\n\n\tcase HOST_TYPE_REDIRECTION:\n\t\t{\n\t\t\tnginxListRedirectionHostsReq := &npmsdk.NginxListRedirectionHostsRequest{}\n\t\t\tnginxListRedirectionHostsResp, err := d.sdkClient.NginxListRedirectionHostsWithContext(ctx, nginxListRedirectionHostsReq)\n\t\t\td.logger.Debug(\"sdk request 'nginx.ListRedirectionHosts'\", slog.Any(\"request\", nginxListRedirectionHostsReq), slog.Any(\"response\", nginxListRedirectionHostsResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'nginx.ListRedirectionHosts': %w\", err)\n\t\t\t}\n\n\t\t\thosts = make([]*npmsdk.HostRecord, 0, len(*nginxListRedirectionHostsResp))\n\t\t\tfor _, hostItem := range *nginxListRedirectionHostsResp {\n\t\t\t\thosts = append(hosts, &hostItem.HostRecord)\n\t\t\t}\n\t\t}\n\n\tcase HOST_TYPE_STREAM:\n\t\t{\n\t\t\tnginxListStreamsReq := &npmsdk.NginxListStreamsRequest{}\n\t\t\tnginxListStreamsResp, err := d.sdkClient.NginxListStreamsWithContext(ctx, nginxListStreamsReq)\n\t\t\td.logger.Debug(\"sdk request 'nginx.ListStreams'\", slog.Any(\"request\", nginxListStreamsReq), slog.Any(\"response\", nginxListStreamsResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'nginx.ListStreams': %w\", err)\n\t\t\t}\n\n\t\t\thosts = make([]*npmsdk.HostRecord, 0, len(*nginxListStreamsResp))\n\t\t\tfor _, hostItem := range *nginxListStreamsResp {\n\t\t\t\thosts = append(hosts, &hostItem.HostRecord)\n\t\t\t}\n\t\t}\n\n\tcase HOST_TYPE_DEAD:\n\t\t{\n\t\t\tnginxListDeadHostsReq := &npmsdk.NginxListDeadHostsRequest{}\n\t\t\tnginxListDeadHostsResp, err := d.sdkClient.NginxListDeadHostsWithContext(ctx, nginxListDeadHostsReq)\n\t\t\td.logger.Debug(\"sdk request 'nginx.ListDeadHosts'\", slog.Any(\"request\", nginxListDeadHostsReq), slog.Any(\"response\", nginxListDeadHostsResp))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'nginx.ListDeadHosts': %w\", err)\n\t\t\t}\n\n\t\t\thosts = make([]*npmsdk.HostRecord, 0, len(*nginxListDeadHostsResp))\n\t\t\tfor _, hostItem := range *nginxListDeadHostsResp {\n\t\t\t\thosts = append(hosts, &hostItem.HostRecord)\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn hosts, fmt.Errorf(\"unsupported host type: '%s'\", cloudHostType)\n\t}\n\n\treturn hosts, nil\n}\n\nfunc (d *Deployer) updateHostCertificate(ctx context.Context, cloudHostType string, cloudHostId int64, cloudCertId int64) error {\n\tswitch cloudHostType {\n\tcase HOST_TYPE_PROXY:\n\t\t{\n\t\t\tnginxUpdateProxyHostReq := &npmsdk.NginxUpdateProxyHostRequest{\n\t\t\t\tCertificateId: lo.ToPtr(cloudCertId),\n\t\t\t}\n\t\t\tnginxUpdateProxyHostResp, err := d.sdkClient.NginxUpdateProxyHostWithContext(ctx, cloudHostId, nginxUpdateProxyHostReq)\n\t\t\td.logger.Debug(\"sdk request 'nginx.UpdateProxyHost'\", slog.Int64(\"request.hostId\", cloudHostId), slog.Any(\"request\", nginxUpdateProxyHostReq), slog.Any(\"response\", nginxUpdateProxyHostResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'nginx.UpdateProxyHost': %w\", err)\n\t\t\t}\n\t\t}\n\n\tcase HOST_TYPE_REDIRECTION:\n\t\t{\n\t\t\tnginxUpdateRedirectionHostReq := &npmsdk.NginxUpdateRedirectionHostRequest{\n\t\t\t\tCertificateId: lo.ToPtr(cloudCertId),\n\t\t\t}\n\t\t\tnginxUpdateRedirectionHostResp, err := d.sdkClient.NginxUpdateRedirectionHostWithContext(ctx, cloudHostId, nginxUpdateRedirectionHostReq)\n\t\t\td.logger.Debug(\"sdk request 'nginx.UpdateRedirectionHost'\", slog.Int64(\"request.hostId\", cloudHostId), slog.Any(\"request\", nginxUpdateRedirectionHostReq), slog.Any(\"response\", nginxUpdateRedirectionHostResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'nginx.UpdateRedirectionHost': %w\", err)\n\t\t\t}\n\t\t}\n\n\tcase HOST_TYPE_STREAM:\n\t\t{\n\t\t\tnginxUpdateStreamReq := &npmsdk.NginxUpdateStreamRequest{\n\t\t\t\tCertificateId: lo.ToPtr(cloudCertId),\n\t\t\t}\n\t\t\tnginxUpdateStreamResp, err := d.sdkClient.NginxUpdateStreamWithContext(ctx, cloudHostId, nginxUpdateStreamReq)\n\t\t\td.logger.Debug(\"sdk request 'nginx.UpdateStream'\", slog.Int64(\"request.hostId\", cloudHostId), slog.Any(\"request\", nginxUpdateStreamReq), slog.Any(\"response\", nginxUpdateStreamResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'nginx.UpdateStream': %w\", err)\n\t\t\t}\n\t\t}\n\n\tcase HOST_TYPE_DEAD:\n\t\t{\n\t\t\tnginxUpdateDeadHostReq := &npmsdk.NginxUpdateDeadHostRequest{\n\t\t\t\tCertificateId: lo.ToPtr(cloudCertId),\n\t\t\t}\n\t\t\tnginxUpdateDeadHostResp, err := d.sdkClient.NginxUpdateDeadHostWithContext(ctx, cloudHostId, nginxUpdateDeadHostReq)\n\t\t\td.logger.Debug(\"sdk request 'nginx.UpdateDeadHost'\", slog.Int64(\"request.hostId\", cloudHostId), slog.Any(\"request\", nginxUpdateDeadHostReq), slog.Any(\"response\", nginxUpdateDeadHostResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'nginx.UpdateDeadHost': %w\", err)\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported host type: '%s'\", cloudHostType)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(serverUrl, authMethod, username, password, apiToken string, skipTlsVerify bool) (*npmsdk.Client, error) {\n\tvar client *npmsdk.Client\n\tvar err error\n\n\tswitch authMethod {\n\tcase \"\", AUTH_METHOD_PASSWORD:\n\t\t{\n\t\t\tclient, err = npmsdk.NewClient(serverUrl, username, password)\n\t\t}\n\n\tcase AUTH_METHOD_TOKEN:\n\t\t{\n\t\t\tclient, err = npmsdk.NewClientWithJwtToken(serverUrl, apiToken)\n\t\t}\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/nginxproxymanager/nginxproxymanager_test.go",
    "content": "package nginxproxymanager_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/nginxproxymanager\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfUsername      string\n\tfPassword      string\n\tfHostType      string\n\tfHostId        int64\n)\n\nfunc init() {\n\targsPrefix := \"NGINXPROXYMANAGER_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fUsername, argsPrefix+\"USERNAME\", \"\", \"\")\n\tflag.StringVar(&fHostType, argsPrefix+\"HOSTTYPE\", \"\", \"\")\n\tflag.Int64Var(&fHostId, argsPrefix+\"HOSTID\", 0, \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./nginxproxymanager_test.go -args \\\n\t--NGINXPROXYMANAGER_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--NGINXPROXYMANAGER_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--NGINXPROXYMANAGER_SERVERURL=\"http://127.0.0.1:20410\" \\\n\t--NGINXPROXYMANAGER_USERNAME=\"your-username\" \\\n\t--NGINXPROXYMANAGER_PASSWORD=\"your-password\" \\\n\t--NGINXPROXYMANAGER_HOSTTYPE=\"proxy\" \\\n\t--NGINXPROXYMANAGER_HOSTID=\"your-host-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"USERNAME: %v\", fUsername),\n\t\t\tfmt.Sprintf(\"PASSWORD: %v\", fPassword),\n\t\t\tfmt.Sprintf(\"HOSTTYPE: %v\", fHostType),\n\t\t\tfmt.Sprintf(\"HOSTID: %v\", fHostId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tAuthMethod:               provider.AUTH_METHOD_PASSWORD,\n\t\t\tUsername:                 fUsername,\n\t\t\tPassword:                 fPassword,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tResourceType:             provider.RESOURCE_TYPE_HOST,\n\t\t\tHostType:                 fHostType,\n\t\t\tHostMatchPattern:         provider.HOST_MATCH_PATTERN_SPECIFIED,\n\t\t\tHostId:                   fHostId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/proxmoxve/proxmoxve.go",
    "content": "package proxmoxve\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/luthermonson/go-proxmox\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\txhttp \"github.com/certimate-go/certimate/pkg/utils/http\"\n)\n\ntype DeployerConfig struct {\n\t// Proxmox VE 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// Proxmox VE API Token。\n\tApiToken string `json:\"apiToken\"`\n\t// Proxmox VE API Token Secret。\n\tApiTokenSecret string `json:\"apiTokenSecret,omitempty\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 集群节点名称。\n\tNodeName string `json:\"nodeName\"`\n\t// 是否自动重启。\n\tAutoRestart bool `json:\"autoRestart\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *proxmox.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiToken, config.ApiTokenSecret, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.NodeName == \"\" {\n\t\treturn nil, errors.New(\"config `nodeName` is required\")\n\t}\n\n\t// 获取节点信息\n\t// REF: https://pve.proxmox.com/pve-docs/api-viewer/index.html#/nodes/{node}\n\tnode, err := d.sdkClient.Node(ctx, d.config.NodeName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get node '%s': %w\", d.config.NodeName, err)\n\t}\n\n\t// 上传自定义证书\n\t// REF: https://pve.proxmox.com/pve-docs/api-viewer/index.html#/nodes/{node}/certificates/custom\n\terr = node.UploadCustomCertificate(ctx, &proxmox.CustomCertificate{\n\t\tCertificates: certPEM,\n\t\tKey:          privkeyPEM,\n\t\tForce:        true,\n\t\tRestart:      d.config.AutoRestart,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload custom certificate to node '%s': %w\", node.Name, err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(serverUrl, apiToken, apiTokenSecret string, skipTlsVerify bool) (*proxmox.Client, error) {\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, errors.New(\"pve: invalid server url\")\n\t}\n\n\tif apiToken == \"\" {\n\t\treturn nil, errors.New(\"pve: invalid api token\")\n\t}\n\n\thttpClient := &http.Client{\n\t\tTransport: xhttp.NewDefaultTransport(),\n\t\tTimeout:   http.DefaultClient.Timeout,\n\t}\n\tif skipTlsVerify {\n\t\ttransport := xhttp.NewDefaultTransport()\n\t\ttransport.DisableKeepAlives = true\n\t\ttransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}\n\t\thttpClient.Transport = transport\n\t}\n\tclient := proxmox.NewClient(\n\t\tstrings.TrimRight(serverUrl, \"/\")+\"/api2/json\",\n\t\tproxmox.WithHTTPClient(httpClient),\n\t\tproxmox.WithAPIToken(apiToken, apiTokenSecret),\n\t)\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/proxmoxve/proxmoxve_test.go",
    "content": "package proxmoxve_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/proxmoxve\"\n)\n\nvar (\n\tfInputCertPath  string\n\tfInputKeyPath   string\n\tfServerUrl      string\n\tfApiToken       string\n\tfApiTokenSecret string\n\tfNodeName       string\n)\n\nfunc init() {\n\targsPrefix := \"PROXMOXVE_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiToken, argsPrefix+\"APITOKEN\", \"\", \"\")\n\tflag.StringVar(&fApiTokenSecret, argsPrefix+\"APITOKENSECRET\", \"\", \"\")\n\tflag.StringVar(&fNodeName, argsPrefix+\"NODENAME\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./proxmoxve_test.go -args \\\n\t--PROXMOXVE_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--PROXMOXVE_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--PROXMOXVE_SERVERURL=\"http://127.0.0.1:8006\" \\\n\t--PROXMOXVE_APITOKEN=\"your-api-token\" \\\n\t--PROXMOXVE_APITOKENSECRET=\"your-api-token-secret\" \\\n\t--PROXMOXVE_NODENAME=\"your-cluster-node-name\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APITOKEN: %v\", fApiToken),\n\t\t\tfmt.Sprintf(\"APITOKENSECRET: %v\", fApiTokenSecret),\n\t\t\tfmt.Sprintf(\"NODENAME: %v\", fNodeName),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiToken:                 fApiToken,\n\t\t\tApiTokenSecret:           fApiTokenSecret,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tNodeName:                 fNodeName,\n\t\t\tAutoRestart:              true,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/qiniu-cdn/consts.go",
    "content": "package qiniucdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go",
    "content": "package qiniucdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/qiniu/go-sdk/v7/auth\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/qiniu-sslcert\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tqiniusdk \"github.com/certimate-go/certimate/pkg/sdk3rd/qiniu\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 七牛云 AccessKey。\n\tAccessKey string `json:\"accessKey\"`\n\t// 七牛云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *qiniusdk.CdnManager\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient := qiniusdk.NewCdnManager(auth.New(config.AccessKey, config.SecretKey))\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKey: config.AccessKey,\n\t\tSecretKey: config.SecretKey,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\t// \"*.example.com\" → \".example.com\"，适配七牛云 CDN 要求的泛域名格式\n\t\t\tdomain := strings.TrimPrefix(d.config.Domain, \"*\")\n\t\t\tdomains = []string{domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain) ||\n\t\t\t\t\t\tstrings.TrimPrefix(d.config.Domain, \"*\") == strings.TrimPrefix(domain, \"*\")\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil ||\n\t\t\t\t\tstrings.TrimPrefix(d.config.Domain, \"*\") == strings.TrimPrefix(domain, \"*\")\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no cdn domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found cdn domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// REF: https://developer.qiniu.com/fusion/4246/the-domain-name\n\tgetDomainListMarker := \"\"\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tgetDomainListResp, err := d.sdkClient.GetDomainList(ctx, getDomainListMarker, 100)\n\t\td.logger.Debug(\"sdk request 'cdn.GetDomainList'\", slog.String(\"request.marker\", getDomainListMarker), slog.Any(\"response\", getDomainListResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.GetDomainList': %w\", err)\n\t\t}\n\n\t\tignoredStatuses := []string{\"frozen\", \"offlined\"}\n\t\tfor _, domainItem := range getDomainListResp.Domains {\n\t\t\tif lo.Contains(ignoredStatuses, domainItem.OperatingState) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdomains = append(domains, domainItem.Name)\n\t\t}\n\n\t\tif len(getDomainListResp.Domains) == 0 || getDomainListResp.Marker == \"\" {\n\t\t\tbreak\n\t\t}\n\n\t\tgetDomainListMarker = getDomainListResp.Marker\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error {\n\t// 获取域名信息\n\t// REF: https://developer.qiniu.com/fusion/4246/the-domain-name\n\tgetDomainInfoResp, err := d.sdkClient.GetDomainInfo(ctx, domain)\n\td.logger.Debug(\"sdk request 'cdn.GetDomainInfo'\", slog.String(\"request.domain\", domain), slog.Any(\"response\", getDomainInfoResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.GetDomainInfo': %w\", err)\n\t}\n\n\t// 判断域名是否已启用 HTTPS\n\t// 如果已启用，修改域名证书；否则，启用 HTTPS\n\t// REF: https://developer.qiniu.com/fusion/4246/the-domain-name\n\tif getDomainInfoResp.Https == nil || getDomainInfoResp.Https.CertID == \"\" {\n\t\tenableDomainHttpsResp, err := d.sdkClient.EnableDomainHttps(ctx, domain, cloudCertId, true, true)\n\t\td.logger.Debug(\"sdk request 'cdn.EnableDomainHttps'\", slog.String(\"request.domain\", domain), slog.String(\"request.certId\", cloudCertId), slog.Any(\"response\", enableDomainHttpsResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.EnableDomainHttps': %w\", err)\n\t\t}\n\t} else if getDomainInfoResp.Https.CertID != cloudCertId {\n\t\tmodifyDomainHttpsConfResp, err := d.sdkClient.ModifyDomainHttpsConf(ctx, domain, cloudCertId, getDomainInfoResp.Https.ForceHttps, getDomainInfoResp.Https.Http2Enable)\n\t\td.logger.Debug(\"sdk request 'cdn.ModifyDomainHttpsConf'\", slog.String(\"request.domain\", domain), slog.String(\"request.certId\", cloudCertId), slog.Any(\"response\", modifyDomainHttpsConfResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.ModifyDomainHttpsConf': %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn_test.go",
    "content": "package qiniucdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-cdn\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfAccessKey     string\n\tfSecretKey     string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"QINIUCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKey, argsPrefix+\"ACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./qiniu_cdn_test.go -args \\\n\t--QINIUCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--QINIUCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--QINIUCDN_ACCESSKEY=\"your-access-key\" \\\n\t--QINIUCDN_SECRETKEY=\"your-secret-key\" \\\n\t--QINIUCDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEY: %v\", fAccessKey),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKey:          fAccessKey,\n\t\t\tSecretKey:          fSecretKey,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/qiniu-kodo/qiniu_kodo.go",
    "content": "package qiniukodo\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/qiniu/go-sdk/v7/auth\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/qiniu-sslcert\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tqiniusdk \"github.com/certimate-go/certimate/pkg/sdk3rd/qiniu\"\n)\n\ntype DeployerConfig struct {\n\t// 七牛云 AccessKey。\n\tAccessKey string `json:\"accessKey\"`\n\t// 七牛云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 存储桶名。暂时无用。\n\tBucket string `json:\"bucket\"`\n\t// 自定义域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *qiniusdk.KodoManager\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient := qiniusdk.NewKodoManager(auth.New(config.AccessKey, config.SecretKey))\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKey: config.AccessKey,\n\t\tSecretKey: config.SecretKey,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.Domain == \"\" {\n\t\treturn nil, fmt.Errorf(\"config `domain` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 绑定空间域名证书\n\tbindBucketCertResp, err := d.sdkClient.BindBucketCert(ctx, d.config.Domain, upres.CertId)\n\td.logger.Debug(\"sdk request 'kodo.BindCert'\", slog.String(\"request.domain\", d.config.Domain), slog.String(\"request.certId\", upres.CertId), slog.Any(\"response\", bindBucketCertResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'kodo.BindCert': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/qiniu-kodo/qiniu_kodo_test.go",
    "content": "package qiniukodo_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-kodo\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfAccessKey     string\n\tfSecretKey     string\n\tfBucket        string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"QINIUKODO_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKey, argsPrefix+\"ACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n\tflag.StringVar(&fBucket, argsPrefix+\"BUCKET\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./qiniu_kodo_test.go -args \\\n\t--QINIUKODO_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--QINIUKODO_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--QINIUKODO_ACCESSKEY=\"your-access-key\" \\\n\t--QINIUKODO_SECRETKEY=\"your-secret-key\" \\\n\t--QINIUKODO_BUCKET=\"your-bucket\" \\\n\t--QINIUKODO_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEY: %v\", fAccessKey),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"BUCKET: %v\", fBucket),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKey: fAccessKey,\n\t\t\tSecretKey: fSecretKey,\n\t\t\tBucket:    fBucket,\n\t\t\tDomain:    fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/qiniu-pili/consts.go",
    "content": "package qiniupili\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/qiniu-pili/qiniu_pili.go",
    "content": "package qiniupili\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/qiniu/go-sdk/v7/pili\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/qiniu-sslcert\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype DeployerConfig struct {\n\t// 七牛云 AccessKey。\n\tAccessKey string `json:\"accessKey\"`\n\t// 七牛云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 直播空间名。\n\tHub string `json:\"hub\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 直播流域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *pili.Manager\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tmanager := pili.NewManager(pili.ManagerConfig{AccessKey: config.AccessKey, SecretKey: config.SecretKey})\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKey: config.AccessKey,\n\t\tSecretKey: config.SecretKey,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  manager,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.Domain == \"\" {\n\t\treturn nil, fmt.Errorf(\"config `domain` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomainsByHub(ctx, d.config.Hub)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no pili domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found pili domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, d.config.Hub, domain, upres.CertName); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomainsByHub(ctx context.Context, hub string) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// REF: https://developer.qiniu.com/pili/9910/pili-service-sdk#6\n\tgetDomainListReq := pili.GetDomainsListRequest{\n\t\tHub: hub,\n\t}\n\tgetDomainListResp, err := d.sdkClient.GetDomainsList(ctx, getDomainListReq)\n\td.logger.Debug(\"sdk request 'pili.GetDomainsList'\", slog.Any(\"request\", getDomainListReq), slog.Any(\"response\", getDomainListResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'pili.GetDomainsList': %w\", err)\n\t}\n\n\tfor _, domainItem := range getDomainListResp.Domains {\n\t\tdomains = append(domains, domainItem.Domain)\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, hub string, domain string, cloudCertName string) error {\n\t// 修改域名证书配置\n\t// REF: https://developer.qiniu.com/pili/9910/pili-service-sdk#6\n\tsetDomainCertReq := pili.SetDomainCertRequest{\n\t\tHub:      hub,\n\t\tDomain:   domain,\n\t\tCertName: cloudCertName,\n\t}\n\terr := d.sdkClient.SetDomainCert(ctx, setDomainCertReq)\n\td.logger.Debug(\"sdk request 'pili.SetDomainCert'\", slog.Any(\"request\", setDomainCertReq))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'pili.SetDomainCert': %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/qiniu-pili/qiniu_pili_test.go",
    "content": "package qiniupili_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-pili\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfAccessKey     string\n\tfSecretKey     string\n\tfHub           string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"QINIUPILI_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKey, argsPrefix+\"ACCESSKEY\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n\tflag.StringVar(&fHub, argsPrefix+\"HUB\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./qiniu_pili_test.go -args \\\n\t--QINIUPILI_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--QINIUPILI_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--QINIUPILI_ACCESSKEY=\"your-access-key\" \\\n\t--QINIUPILI_SECRETKEY=\"your-secret-key\" \\\n\t--QINIUPILI_HUB=\"your-hub-name\" \\\n\t--QINIUPILI_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEY: %v\", fAccessKey),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"HUB: %v\", fHub),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKey:          fAccessKey,\n\t\t\tSecretKey:          fSecretKey,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/rainyun-rcdn/consts.go",
    "content": "package rainyunrcdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go",
    "content": "package rainyunrcdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/rainyun-sslcenter\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\trainyunsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/rainyun\"\n)\n\ntype DeployerConfig struct {\n\t// 雨云 API 密钥。\n\tApiKey string `json:\"apiKey\"`\n\t// RCDN 实例 ID。\n\tInstanceId int64 `json:\"instanceId\"`\n\t// 域名匹配模式。暂时只支持精确匹配。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *rainyunsdk.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ApiKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tApiKey: config.ApiKey,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.InstanceId == 0 {\n\t\treturn nil, fmt.Errorf(\"config `instanceId` is required\")\n\t}\n\tif d.config.Domain == \"\" {\n\t\treturn nil, fmt.Errorf(\"config `domain` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// RCDN SSL 绑定域名\n\t// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-184214120\n\tcertId, _ := strconv.ParseInt(upres.CertId, 10, 64)\n\trcdnInstanceSslBindReq := &rainyunsdk.RcdnInstanceSslBindRequest{\n\t\tCertId:  certId,\n\t\tDomains: []string{d.config.Domain},\n\t}\n\trcdnInstanceSslBindResp, err := d.sdkClient.RcdnInstanceSslBindWithContext(ctx, d.config.InstanceId, rcdnInstanceSslBindReq)\n\td.logger.Debug(\"sdk request 'rcdn.InstanceSslBind'\", slog.Int64(\"instanceId\", d.config.InstanceId), slog.Any(\"request\", rcdnInstanceSslBindReq), slog.Any(\"response\", rcdnInstanceSslBindResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'rcdn.InstanceSslBind': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(apiKey string) (*rainyunsdk.Client, error) {\n\treturn rainyunsdk.NewClient(apiKey)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn_test.go",
    "content": "package rainyunrcdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/rainyun-rcdn\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfApiKey        string\n\tfInstanceId    int64\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"RAINYUNRCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n\tflag.Int64Var(&fInstanceId, argsPrefix+\"INSTANCEID\", 0, \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./rainyun_rcdn_test.go -args \\\n\t--RAINYUNRCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--RAINYUNRCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--RAINYUNRCDN_APIKEY=\"your-api-key\" \\\n\t--RAINYUNRCDN_INSTANCEID=\"your-rcdn-instance-id\" \\\n\t--RAINYUNRCDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t\tfmt.Sprintf(\"INSTANCEID: %v\", fInstanceId),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tApiKey:             fApiKey,\n\t\t\tInstanceId:         fInstanceId,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/rainyun-sslcenter/rainyun_sslcenter.go",
    "content": "package rainyunsslcenter\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/rainyun-sslcenter\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// 雨云 API 密钥。\n\tApiKey string `json:\"apiKey\"`\n\t// 证书 ID。\n\t// 选填。零值时表示新建证书；否则表示更新证书。\n\tCertificateId int64 `json:\"certificateId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tApiKey: config.ApiKey,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.CertificateId == 0 {\n\t\t// 上传证书\n\t\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t\t} else {\n\t\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t\t}\n\t} else {\n\t\t// 替换证书\n\t\topres, err := d.sdkCertmgr.Replace(ctx, strconv.FormatInt(d.config.CertificateId, 10), certPEM, privkeyPEM)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to replace certificate file: %w\", err)\n\t\t} else {\n\t\t\td.logger.Info(\"ssl certificate replaced\", slog.Any(\"result\", opres))\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/rainyun-sslcenter/rainyun_sslcenter_test.go",
    "content": "package rainyunsslcenter_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/rainyun-sslcenter\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfApiKey        string\n)\n\nfunc init() {\n\targsPrefix := \"RAINYUNSSLCENTER_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./rainyun_sslcenter_test.go -args \\\n\t--RAINYUNRCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--RAINYUNRCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--RAINYUNRCDN_APIKEY=\"your-api-key\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tApiKey: fApiKey,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ratpanel/consts.go",
    "content": "package ratpanel\n\nconst (\n\t// 资源类型：替换指定网站的证书。\n\tRESOURCE_TYPE_WEBSITE = \"website\"\n\t// 资源类型：替换指定证书。\n\tRESOURCE_TYPE_CERTIFICATE = \"certificate\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/ratpanel/ratpanel.go",
    "content": "package ratpanel\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tratpanelsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/ratpanel\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype DeployerConfig struct {\n\t// 耗子面板服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// 耗子面板访问令牌 ID。\n\tAccessTokenId int64 `json:\"accessTokenId\"`\n\t// 耗子面板访问令牌。\n\tAccessToken string `json:\"accessToken\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 网站名称。\n\t// 部署资源类型为 [RESOURCE_TYPE_WEBSITE] 时必填。\n\tSiteNames []string `json:\"siteNames,omitempty\"`\n\t// 证书 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。\n\tCertificateId int64 `json:\"certificateId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *ratpanelsdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.AccessTokenId, config.AccessToken, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_WEBSITE:\n\t\tif err := d.deployToWebsite(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_CERTIFICATE:\n\t\tif err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToWebsite(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif len(d.config.SiteNames) == 0 {\n\t\treturn errors.New(\"config `siteNames` is required\")\n\t}\n\n\t// 遍历更新站点证书\n\tvar errs []error\n\tfor _, siteName := range d.config.SiteNames {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t\tif err := d.updateSiteCertificate(ctx, siteName, certPEM, privkeyPEM); err != nil {\n\t\t\t\terrs = append(errs, err)\n\t\t\t}\n\t\t}\n\t}\n\tif len(errs) > 0 {\n\t\treturn errors.Join(errs...)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.CertificateId == 0 {\n\t\treturn errors.New(\"config `certificateId` is required\")\n\t}\n\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 更新 SSL 证书\n\tcertUpdateReq := &ratpanelsdk.CertUpdateRequest{\n\t\tCertId:      d.config.CertificateId,\n\t\tType:        \"upload\",\n\t\tDomains:     certX509.DNSNames,\n\t\tCertificate: certPEM,\n\t\tPrivateKey:  privkeyPEM,\n\t}\n\tcertUpdateResp, err := d.sdkClient.CertUpdateWithContext(ctx, certUpdateReq)\n\td.logger.Debug(\"sdk request 'ratpanel.CertUpdate'\", slog.Any(\"request\", certUpdateReq), slog.Any(\"response\", certUpdateResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'ratpanel.CertUpdate': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateSiteCertificate(ctx context.Context, siteName string, certPEM, privkeyPEM string) error {\n\t// 设置站点 SSL 证书\n\tsetWebsiteCertReq := &ratpanelsdk.SetWebsiteCertRequest{\n\t\tSiteName:    siteName,\n\t\tCertificate: certPEM,\n\t\tPrivateKey:  privkeyPEM,\n\t}\n\tsetWebsiteCertResp, err := d.sdkClient.SetWebsiteCertWithContext(ctx, setWebsiteCertReq)\n\td.logger.Debug(\"sdk request 'ratpanel.SetWebsiteCert'\", slog.Any(\"request\", setWebsiteCertReq), slog.Any(\"response\", setWebsiteCertResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'ratpanel.SetWebsiteCert': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(serverUrl string, accessTokenId int64, accessToken string, skipTlsVerify bool) (*ratpanelsdk.Client, error) {\n\tclient, err := ratpanelsdk.NewClient(serverUrl, accessTokenId, accessToken)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ratpanel/ratpanel_test.go",
    "content": "package ratpanel_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ratpanel\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfAccessTokenId int64\n\tfAccessToken   string\n\tfSiteName      string\n)\n\nfunc init() {\n\targsPrefix := \"RATPANEL_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.Int64Var(&fAccessTokenId, argsPrefix+\"ACCESSTOKENID\", 0, \"\")\n\tflag.StringVar(&fAccessToken, argsPrefix+\"ACCESSTOKEN\", \"\", \"\")\n\tflag.StringVar(&fSiteName, argsPrefix+\"SITENAME\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ratpanel_test.go -args \\\n\t--RATPANEL_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--RATPANEL_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--RATPANEL_SERVERURL=\"http://127.0.0.1:8888\" \\\n\t--RATPANEL_ACCESSTOKENID=\"your-access-token-id\" \\\n\t--RATPANEL_ACCESSTOKEN=\"your-access-token\" \\\n\t--RATPANEL_SITENAME=\"your-site-name\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"ACCESSTOKENID: %v\", fAccessTokenId),\n\t\t\tfmt.Sprintf(\"ACCESSTOKEN: %v\", fAccessToken),\n\t\t\tfmt.Sprintf(\"SITENAME: %v\", fSiteName),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tAccessTokenId:            fAccessTokenId,\n\t\t\tAccessToken:              fAccessToken,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tResourceType:             provider.RESOURCE_TYPE_WEBSITE,\n\t\t\tSiteNames:                []string{fSiteName},\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ratpanel-console/ratpanel_console.go",
    "content": "package ratpanelconsole\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tratpanelsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/ratpanel\"\n)\n\ntype DeployerConfig struct {\n\t// 耗子面板服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// 耗子面板访问令牌 ID。\n\tAccessTokenId int64 `json:\"accessTokenId\"`\n\t// 耗子面板访问令牌。\n\tAccessToken string `json:\"accessToken\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *ratpanelsdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.AccessTokenId, config.AccessToken, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 设置面板 SSL 证书\n\tsetSettingCertReq := &ratpanelsdk.SetSettingCertRequest{\n\t\tCertificate: certPEM,\n\t\tPrivateKey:  privkeyPEM,\n\t}\n\tsetSettingCertResp, err := d.sdkClient.SetSettingCertWithContext(ctx, setSettingCertReq)\n\td.logger.Debug(\"sdk request 'ratpanel.SetSettingCert'\", slog.Any(\"request\", setSettingCertReq), slog.Any(\"response\", setSettingCertResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'ratpanel.SetSettingCert': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(serverUrl string, accessTokenId int64, accessToken string, skipTlsVerify bool) (*ratpanelsdk.Client, error) {\n\tclient, err := ratpanelsdk.NewClient(serverUrl, accessTokenId, accessToken)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ratpanel-console/ratpanel_console_test.go",
    "content": "package ratpanelconsole_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ratpanel-console\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfAccessTokenId int64\n\tfAccessToken   string\n)\n\nfunc init() {\n\targsPrefix := \"RATPANELCONSOLE_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.Int64Var(&fAccessTokenId, argsPrefix+\"ACCESSTOKENID\", 0, \"\")\n\tflag.StringVar(&fAccessToken, argsPrefix+\"ACCESSTOKEN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ratpanel_console_test.go -args \\\n\t--RATPANELCONSOLE_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--RATPANELCONSOLE_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--RATPANELCONSOLE_SERVERURL=\"http://127.0.0.1:8888\" \\\n\t--RATPANELCONSOLE_ACCESSTOKENID=\"your-access-token-id\" \\\n\t--RATPANELCONSOLE_ACCESSTOKEN=\"your-access-token\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"ACCESSTOKENID: %v\", fAccessTokenId),\n\t\t\tfmt.Sprintf(\"ACCESSTOKEN: %v\", fAccessToken),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tAccessTokenId:            fAccessTokenId,\n\t\t\tAccessToken:              fAccessToken,\n\t\t\tAllowInsecureConnections: true,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/s3/consts.go",
    "content": "package s3\n\nimport (\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\nconst (\n\tOUTPUT_FORMAT_PEM = string(domain.CertificateFormatTypePEM)\n\tOUTPUT_FORMAT_PFX = string(domain.CertificateFormatTypePFX)\n\tOUTPUT_FORMAT_JKS = string(domain.CertificateFormatTypeJKS)\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/s3/s3.go",
    "content": "package s3\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/internal/tools/s3\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype DeployerConfig struct {\n\t// S3 Endpoint。\n\tEndpoint string `json:\"endpoint\"`\n\t// S3 AccessKey。\n\tAccessKey string `json:\"accessKey\"`\n\t// S3 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// S3 签名版本。\n\t// 可取值 \"v2\"、\"v4\"。\n\t// 零值时默认值 \"v4\"。\n\tSignatureVersion string `json:\"signatureVersion,omitempty\"`\n\t// 是否使用路径风格。\n\tUsePathStyle bool `json:\"usePathStyle,omitempty\"`\n\t// 存储区域。\n\tRegion string `json:\"region\"`\n\t// 存储桶名。\n\tBucket string `json:\"bucket\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 输出证书格式。\n\tOutputFormat string `json:\"outputFormat,omitempty\"`\n\t// 输出证书文件路径。\n\tOutputCertObjectKey string `json:\"outputCertObjectKey,omitempty\"`\n\t// 输出服务器证书文件路径。\n\t// 选填。\n\tOutputServerCertObjectKey string `json:\"outputServerCertObjectKey,omitempty\"`\n\t// 输出中间证书文件路径。\n\t// 选填。\n\tOutputIntermediaCertObjectKey string `json:\"outputIntermediaCertObjectKey,omitempty\"`\n\t// 输出私钥文件路径。\n\tOutputKeyObjectKey string `json:\"outputKeyObjectKey,omitempty\"`\n\t// PFX 导出密码。\n\t// 证书格式为 PFX 时必填。\n\tPfxPassword string `json:\"pfxPassword,omitempty\"`\n\t// JKS 别名。\n\t// 证书格式为 JKS 时必填。\n\tJksAlias string `json:\"jksAlias,omitempty\"`\n\t// JKS 密钥密码。\n\t// 证书格式为 JKS 时必填。\n\tJksKeypass string `json:\"jksKeypass,omitempty\"`\n\t// JKS 存储密码。\n\t// 证书格式为 JKS 时必填。\n\tJksStorepass string `json:\"jksStorepass,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig   *DeployerConfig\n\tlogger   *slog.Logger\n\ts3Client *s3.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createS3Client(*config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"s3: failed to create S3 client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:   config,\n\t\tlogger:   slog.Default(),\n\t\ts3Client: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 提取服务器证书和中间证书\n\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to extract certs: %w\", err)\n\t}\n\n\t// 写入证书和私钥文件\n\tswitch d.config.OutputFormat {\n\tcase OUTPUT_FORMAT_PEM:\n\t\t{\n\t\t\tif err := d.s3Client.PutObjectString(ctx, d.config.Bucket, d.config.OutputCertObjectKey, certPEM); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl certificate file uploaded\", slog.String(\"bucket\", d.config.Bucket), slog.String(\"object\", d.config.OutputCertObjectKey))\n\n\t\t\tif d.config.OutputServerCertObjectKey != \"\" {\n\t\t\t\tif err := d.s3Client.PutObjectString(ctx, d.config.Bucket, d.config.OutputServerCertObjectKey, serverCertPEM); err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to upload server certificate file: %w\", err)\n\t\t\t\t}\n\t\t\t\td.logger.Info(\"ssl server certificate file uploaded\", slog.String(\"bucket\", d.config.Bucket), slog.String(\"object\", d.config.OutputServerCertObjectKey))\n\t\t\t}\n\n\t\t\tif d.config.OutputIntermediaCertObjectKey != \"\" {\n\t\t\t\tif err := d.s3Client.PutObjectString(ctx, d.config.Bucket, d.config.OutputIntermediaCertObjectKey, intermediaCertPEM); err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to upload intermedia certificate file: %w\", err)\n\t\t\t\t}\n\t\t\t\td.logger.Info(\"ssl intermedia certificate file uploaded\", slog.String(\"bucket\", d.config.Bucket), slog.String(\"object\", d.config.OutputIntermediaCertObjectKey))\n\t\t\t}\n\n\t\t\tif err := d.s3Client.PutObjectString(ctx, d.config.Bucket, d.config.OutputKeyObjectKey, privkeyPEM); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to upload private key file: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl private key file uploaded\", slog.String(\"bucket\", d.config.Bucket), slog.String(\"object\", d.config.OutputKeyObjectKey))\n\t\t}\n\n\tcase OUTPUT_FORMAT_PFX:\n\t\t{\n\t\t\tpfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to transform certificate to PFX: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl certificate transformed to pfx\")\n\n\t\t\tif err := d.s3Client.PutObjectBytes(ctx, d.config.Bucket, d.config.OutputCertObjectKey, pfxData); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl certificate file uploaded\", slog.String(\"bucket\", d.config.Bucket), slog.String(\"object\", d.config.OutputCertObjectKey))\n\t\t}\n\n\tcase OUTPUT_FORMAT_JKS:\n\t\t{\n\t\t\tjksData, err := xcert.TransformCertificateFromPEMToJKS(certPEM, privkeyPEM, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to transform certificate to JKS: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl certificate transformed to jks\")\n\n\t\t\tif err := d.s3Client.PutObjectBytes(ctx, d.config.Bucket, d.config.OutputCertObjectKey, jksData); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl certificate file uploaded\", slog.String(\"bucket\", d.config.Bucket), slog.String(\"object\", d.config.OutputCertObjectKey))\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported output format '%s'\", d.config.OutputFormat)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createS3Client(config DeployerConfig) (*s3.Client, error) {\n\tclientCfg := s3.NewDefaultConfig()\n\tclientCfg.Endpoint = config.Endpoint\n\tclientCfg.AccessKey = config.AccessKey\n\tclientCfg.SecretKey = config.SecretKey\n\tclientCfg.SignatureVersion = config.SignatureVersion\n\tclientCfg.UsePathStyle = config.UsePathStyle\n\tclientCfg.Region = config.Region\n\tclientCfg.SkipTlsVerify = config.AllowInsecureConnections\n\n\tclient, err := s3.NewClient(clientCfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, err\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/safeline/consts.go",
    "content": "package safeline\n\nconst (\n\t// 资源类型：替换指定证书。\n\tRESOURCE_TYPE_CERTIFICATE = \"certificate\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/safeline/safeline.go",
    "content": "package safeline\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tsafelinesdk \"github.com/certimate-go/certimate/pkg/sdk3rd/safeline\"\n)\n\ntype DeployerConfig struct {\n\t// 雷池服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// 雷池 API Token。\n\tApiToken string `json:\"apiToken\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 证书 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。\n\tCertificateId int64 `json:\"certificateId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *safelinesdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.ApiToken, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 根据部署资源类型决定部署方式``\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_CERTIFICATE:\n\t\tif err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error {\n\tif d.config.CertificateId == 0 {\n\t\treturn errors.New(\"config `certificateId` is required\")\n\t}\n\n\t// 更新证书\n\tupdateCertificateReq := &safelinesdk.UpdateCertificateRequest{\n\t\tId:   d.config.CertificateId,\n\t\tType: 2,\n\t\tManual: &safelinesdk.CertificateManul{\n\t\t\tCrt: certPEM,\n\t\t\tKey: privkeyPEM,\n\t\t},\n\t}\n\tupdateCertificateResp, err := d.sdkClient.UpdateCertificateWithContext(ctx, updateCertificateReq)\n\td.logger.Debug(\"sdk request 'safeline.UpdateCertificate'\", slog.Any(\"request\", updateCertificateReq), slog.Any(\"response\", updateCertificateResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'safeline.UpdateCertificate': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(serverUrl, apiToken string, skipTlsVerify bool) (*safelinesdk.Client, error) {\n\tclient, err := safelinesdk.NewClient(serverUrl, apiToken)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/safeline/safeline_test.go",
    "content": "package safeline_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/safeline\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfServerUrl     string\n\tfApiToken      string\n\tfCertificateId int64\n)\n\nfunc init() {\n\targsPrefix := \"SAFELINE_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fApiToken, argsPrefix+\"APITOKEN\", \"\", \"\")\n\tflag.Int64Var(&fCertificateId, argsPrefix+\"CERTIFICATEID\", 0, \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./safeline_test.go -args \\\n\t--SAFELINE_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--SAFELINE_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--SAFELINE_SERVERURL=\"http://127.0.0.1:9443\" \\\n\t--SAFELINE_APITOKEN=\"your-api-token\" \\\n\t--SAFELINE_CERTIFICATEID=\"your-certificate-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"APITOKEN: %v\", fApiToken),\n\t\t\tfmt.Sprintf(\"CERTIFICATEID: %v\", fCertificateId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                fServerUrl,\n\t\t\tApiToken:                 fApiToken,\n\t\t\tAllowInsecureConnections: true,\n\t\t\tResourceType:             provider.RESOURCE_TYPE_CERTIFICATE,\n\t\t\tCertificateId:            fCertificateId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ssh/consts.go",
    "content": "package ssh\n\nimport (\n\t\"github.com/certimate-go/certimate/internal/domain\"\n)\n\nconst (\n\tOUTPUT_FORMAT_PEM = string(domain.CertificateFormatTypePEM)\n\tOUTPUT_FORMAT_PFX = string(domain.CertificateFormatTypePFX)\n\tOUTPUT_FORMAT_JKS = string(domain.CertificateFormatTypeJKS)\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/ssh/ssh.go",
    "content": "package ssh\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/certimate-go/certimate/internal/tools/ssh\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txssh \"github.com/certimate-go/certimate/pkg/utils/ssh\"\n)\n\ntype ServerConfig struct {\n\t// SSH 主机。\n\t// 零值时默认值 \"localhost\"。\n\tSshHost string `json:\"sshHost,omitempty\"`\n\t// SSH 端口。\n\t// 零值时默认值 22。\n\tSshPort int32 `json:\"sshPort,omitempty\"`\n\t// SSH 认证方式。\n\t// 可取值 \"none\"、\"password\"、\"key\"。\n\t// 零值时根据有无密码或私钥字段决定。\n\tSshAuthMethod string `json:\"sshAuthMethod,omitempty\"`\n\t// SSH 登录用户名。\n\t// 零值时默认值 \"root\"。\n\tSshUsername string `json:\"sshUsername,omitempty\"`\n\t// SSH 登录密码。\n\tSshPassword string `json:\"sshPassword,omitempty\"`\n\t// SSH 登录私钥。\n\tSshKey string `json:\"sshKey,omitempty\"`\n\t// SSH 登录私钥口令。\n\tSshKeyPassphrase string `json:\"sshKeyPassphrase,omitempty\"`\n}\n\ntype DeployerConfig struct {\n\tServerConfig\n\n\t// 跳板机配置数组。\n\tJumpServers []ServerConfig `json:\"jumpServers,omitempty\"`\n\t// 是否回退使用 SCP。\n\tUseSCP bool `json:\"useSCP,omitempty\"`\n\t// 前置命令。\n\tPreCommand string `json:\"preCommand,omitempty\"`\n\t// 后置命令。\n\tPostCommand string `json:\"postCommand,omitempty\"`\n\t// 输出证书格式。\n\tOutputFormat string `json:\"outputFormat,omitempty\"`\n\t// 输出私钥文件路径。\n\tOutputKeyPath string `json:\"outputKeyPath,omitempty\"`\n\t// 输出证书文件路径。\n\tOutputCertPath string `json:\"outputCertPath,omitempty\"`\n\t// 输出服务器证书文件路径。\n\t// 选填。\n\tOutputServerCertPath string `json:\"outputServerCertPath,omitempty\"`\n\t// 输出中间证书文件路径。\n\t// 选填。\n\tOutputIntermediaCertPath string `json:\"outputIntermediaCertPath,omitempty\"`\n\t// PFX 导出密码。\n\t// 证书格式为 PFX 时必填。\n\tPfxPassword string `json:\"pfxPassword,omitempty\"`\n\t// JKS 别名。\n\t// 证书格式为 JKS 时必填。\n\tJksAlias string `json:\"jksAlias,omitempty\"`\n\t// JKS 密钥密码。\n\t// 证书格式为 JKS 时必填。\n\tJksKeypass string `json:\"jksKeypass,omitempty\"`\n\t// JKS 存储密码。\n\t// 证书格式为 JKS 时必填。\n\tJksStorepass string `json:\"jksStorepass,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig *DeployerConfig\n\tlogger *slog.Logger\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\treturn &Deployer{\n\t\tconfig: config,\n\t\tlogger: slog.Default(),\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 提取服务器证书和中间证书\n\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to extract certs: %w\", err)\n\t}\n\n\tclient, err := createSshClient(*d.config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ssh: failed to create SSH client: %w\", err)\n\t}\n\n\td.logger.Info(\"ssh connected\")\n\n\t// 执行前置命令\n\tif d.config.PreCommand != \"\" {\n\t\tcommand := d.config.PreCommand\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}\", d.config.OutputCertPath)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}\", d.config.OutputServerCertPath)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_INTERMEDIA_PATH}\", d.config.OutputIntermediaCertPath)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}\", d.config.OutputKeyPath)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}\", d.config.PfxPassword)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_JKS_ALIAS}\", d.config.JksAlias)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_JKS_KEYPASS}\", d.config.JksKeypass)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_JKS_STOREPASS}\", d.config.JksStorepass)\n\n\t\tstdout, stderr, err := xssh.RunCommand(client.GetClient(), command)\n\t\td.logger.Debug(\"run pre-command\", slog.String(\"stdout\", stdout), slog.String(\"stderr\", stderr))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute pre-command (stdout: %s, stderr: %s): %w \", stdout, stderr, err)\n\t\t}\n\t}\n\n\t// 上传证书和私钥文件\n\tswitch d.config.OutputFormat {\n\tcase OUTPUT_FORMAT_PEM:\n\t\t{\n\t\t\tif err := xssh.WriteRemoteString(client.GetClient(), d.config.OutputCertPath, certPEM, d.config.UseSCP); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl certificate file uploaded\", slog.String(\"path\", d.config.OutputCertPath))\n\n\t\t\tif d.config.OutputServerCertPath != \"\" {\n\t\t\t\tif err := xssh.WriteRemoteString(client.GetClient(), d.config.OutputServerCertPath, serverCertPEM, d.config.UseSCP); err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to save server certificate file: %w\", err)\n\t\t\t\t}\n\t\t\t\td.logger.Info(\"ssl server certificate file uploaded\", slog.String(\"path\", d.config.OutputServerCertPath))\n\t\t\t}\n\n\t\t\tif d.config.OutputIntermediaCertPath != \"\" {\n\t\t\t\tif err := xssh.WriteRemoteString(client.GetClient(), d.config.OutputIntermediaCertPath, intermediaCertPEM, d.config.UseSCP); err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to save intermedia certificate file: %w\", err)\n\t\t\t\t}\n\t\t\t\td.logger.Info(\"ssl intermedia certificate file uploaded\", slog.String(\"path\", d.config.OutputIntermediaCertPath))\n\t\t\t}\n\n\t\t\tif err := xssh.WriteRemoteString(client.GetClient(), d.config.OutputKeyPath, privkeyPEM, d.config.UseSCP); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to upload private key file: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl private key file uploaded\", slog.String(\"path\", d.config.OutputKeyPath))\n\t\t}\n\n\tcase OUTPUT_FORMAT_PFX:\n\t\t{\n\t\t\tpfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to transform certificate to PFX: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl certificate transformed to pfx\")\n\n\t\t\tif err := xssh.WriteRemote(client.GetClient(), d.config.OutputCertPath, pfxData, d.config.UseSCP); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl certificate file uploaded\", slog.String(\"path\", d.config.OutputCertPath))\n\t\t}\n\n\tcase OUTPUT_FORMAT_JKS:\n\t\t{\n\t\t\tjksData, err := xcert.TransformCertificateFromPEMToJKS(certPEM, privkeyPEM, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to transform certificate to JKS: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl certificate transformed to jks\")\n\n\t\t\tif err := xssh.WriteRemote(client.GetClient(), d.config.OutputCertPath, jksData, d.config.UseSCP); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t\t\t}\n\t\t\td.logger.Info(\"ssl certificate file uploaded\", slog.String(\"path\", d.config.OutputCertPath))\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported output format '%s'\", d.config.OutputFormat)\n\t}\n\n\t// 执行后置命令\n\tif d.config.PostCommand != \"\" {\n\t\tcommand := d.config.PostCommand\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}\", d.config.OutputCertPath)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}\", d.config.OutputServerCertPath)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_INTERMEDIA_PATH}\", d.config.OutputIntermediaCertPath)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}\", d.config.OutputKeyPath)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}\", d.config.PfxPassword)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_JKS_ALIAS}\", d.config.JksAlias)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_JKS_KEYPASS}\", d.config.JksKeypass)\n\t\tcommand = strings.ReplaceAll(command, \"${CERTIMATE_DEPLOYER_CMDVAR_JKS_STOREPASS}\", d.config.JksStorepass)\n\n\t\tstdout, stderr, err := xssh.RunCommand(client.GetClient(), command)\n\t\td.logger.Debug(\"run post-command\", slog.String(\"stdout\", stdout), slog.String(\"stderr\", stderr))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute post-command (stdout: %s, stderr: %s): %w \", stdout, stderr, err)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSshClient(config DeployerConfig) (*ssh.Client, error) {\n\tclientCfg := ssh.NewDefaultConfig()\n\tclientCfg.Host = config.SshHost\n\tclientCfg.Port = int(config.SshPort)\n\tclientCfg.AuthMethod = ssh.AuthMethodType(config.SshAuthMethod)\n\tclientCfg.Username = config.SshUsername\n\tclientCfg.Password = config.SshPassword\n\tclientCfg.Key = config.SshKey\n\tclientCfg.KeyPassphrase = config.SshKeyPassphrase\n\tfor _, jumpServer := range config.JumpServers {\n\t\tjumpServerCfg := ssh.NewServerConfig()\n\t\tjumpServerCfg.Host = jumpServer.SshHost\n\t\tjumpServerCfg.Port = int(jumpServer.SshPort)\n\t\tjumpServerCfg.AuthMethod = ssh.AuthMethodType(jumpServer.SshAuthMethod)\n\t\tjumpServerCfg.Username = jumpServer.SshUsername\n\t\tjumpServerCfg.Password = jumpServer.SshPassword\n\t\tjumpServerCfg.Key = jumpServer.SshKey\n\t\tjumpServerCfg.KeyPassphrase = jumpServer.SshKeyPassphrase\n\t\tclientCfg.JumpServers = append(clientCfg.JumpServers, *jumpServerCfg)\n\t}\n\n\tclient, err := ssh.NewClient(clientCfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ssh/ssh_test.go",
    "content": "package ssh_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ssh\"\n)\n\nvar (\n\tfInputCertPath  string\n\tfInputKeyPath   string\n\tfSshHost        string\n\tfSshPort        int64\n\tfSshUsername    string\n\tfSshPassword    string\n\tfOutputCertPath string\n\tfOutputKeyPath  string\n)\n\nfunc init() {\n\targsPrefix := \"SSH_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fSshHost, argsPrefix+\"SSHHOST\", \"\", \"\")\n\tflag.Int64Var(&fSshPort, argsPrefix+\"SSHPORT\", 0, \"\")\n\tflag.StringVar(&fSshUsername, argsPrefix+\"SSHUSERNAME\", \"\", \"\")\n\tflag.StringVar(&fSshPassword, argsPrefix+\"SSHPASSWORD\", \"\", \"\")\n\tflag.StringVar(&fOutputCertPath, argsPrefix+\"OUTPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fOutputKeyPath, argsPrefix+\"OUTPUTKEYPATH\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ssh_test.go -args \\\n\t--SSH_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--SSH_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--SSH_SSHHOST=\"localhost\" \\\n\t--SSH_SSHPORT=22 \\\n\t--SSH_SSHUSERNAME=\"root\" \\\n\t--SSH_SSHPASSWORD=\"password\" \\\n\t--SSH_OUTPUTCERTPATH=\"/path/to/your-output-cert.pem\" \\\n\t--SSH_OUTPUTKEYPATH=\"/path/to/your-output-key.pem\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SSHHOST: %v\", fSshHost),\n\t\t\tfmt.Sprintf(\"SSHPORT: %v\", fSshPort),\n\t\t\tfmt.Sprintf(\"SSHUSERNAME: %v\", fSshUsername),\n\t\t\tfmt.Sprintf(\"SSHPASSWORD: %v\", fSshPassword),\n\t\t\tfmt.Sprintf(\"OUTPUTCERTPATH: %v\", fOutputCertPath),\n\t\t\tfmt.Sprintf(\"OUTPUTKEYPATH: %v\", fOutputKeyPath),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerConfig: provider.ServerConfig{\n\t\t\t\tSshHost:     fSshHost,\n\t\t\t\tSshPort:     int32(fSshPort),\n\t\t\t\tSshUsername: fSshUsername,\n\t\t\t\tSshPassword: fSshPassword,\n\t\t\t},\n\t\t\tOutputFormat:   provider.OUTPUT_FORMAT_PEM,\n\t\t\tOutputCertPath: fOutputCertPath,\n\t\t\tOutputKeyPath:  fOutputKeyPath,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/synologydsm/synologydsm.go",
    "content": "package synologydsm\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/pquerna/otp/totp\"\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tdsmsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/synologydsm\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txwait \"github.com/certimate-go/certimate/pkg/utils/wait\"\n)\n\ntype DeployerConfig struct {\n\t// 群晖 DSM 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// 群晖 DSM 用户名。\n\tUsername string `json:\"username\"`\n\t// 群晖 DSM 用户密码。\n\tPassword string `json:\"password\"`\n\t// 群晖 DSM 2FA TOTP 密钥。\n\tTotpSecret string `json:\"totpSecret,omitempty\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n\t// 证书 ID 或描述。\n\t// 选填。零值时表示新建证书；否则表示更新证书。\n\tCertificateIdOrDescription string `json:\"certificateIdOrDesc,omitempty\"`\n\t// 是否设为默认证书。\n\tIsDefault bool `json:\"isDefault,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *dsmsdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.ServerUrl, config.AllowInsecureConnections)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.Default()\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 提取服务器证书和中间证书\n\tserverCertPEM, intermediateCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to extract certs: %w\", err)\n\t}\n\n\t// 如果启用了 TOTP，则等到下一个时间窗口后生成 OTP 动态密码\n\tvar otpCode string\n\tif d.config.TotpSecret != \"\" {\n\t\tnow := time.Now()\n\t\twait := time.Duration(30-now.Unix()%30) * time.Second\n\t\tif wait > 0 {\n\t\t\twait = wait + 1*time.Second\n\t\t\td.logger.Info(\"waiting for the next TOTP time step ...\", slog.Int(\"wait\", int(wait.Seconds())))\n\t\t\txwait.DelayWithContext(ctx, wait)\n\t\t}\n\n\t\tnow = time.Now()\n\t\totpCodeStr, err := totp.GenerateCode(d.config.TotpSecret, now)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to generate TOTP code: %w\", err)\n\t\t}\n\n\t\totpCode = otpCodeStr\n\t}\n\n\t// 登录到群晖 DSM\n\tloginReq := &dsmsdk.LoginRequest{\n\t\tAccount:  d.config.Username,\n\t\tPassword: d.config.Password,\n\t\tOtpCode:  otpCode,\n\t}\n\tloginResp, err := d.sdkClient.Login(loginReq)\n\td.logger.Debug(\"sdk request 'SYNO.API.Auth:login'\", slog.Any(\"request\", loginReq), slog.Any(\"response\", loginResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'SYNO.API.Auth:login': %w\", err)\n\t}\n\tdefer func() {\n\t\tlogoutResp, _ := d.sdkClient.Logout()\n\t\td.logger.Debug(\"sdk request 'SYNO.API.Auth:logout'\", slog.Any(\"response\", logoutResp))\n\t}()\n\n\t// 如果原证书 ID 或描述为空，则创建证书；否则更新证书。\n\tif d.config.CertificateIdOrDescription == \"\" {\n\t\t// 导入证书\n\t\timportCertificateReq := &dsmsdk.ImportCertificateRequest{\n\t\t\tID:          \"\",\n\t\t\tDescription: fmt.Sprintf(\"certimate-%d\", time.Now().UnixMilli()),\n\t\t\tKey:         privkeyPEM,\n\t\t\tCert:        serverCertPEM,\n\t\t\tInterCert:   intermediateCertPEM,\n\t\t\tAsDefault:   d.config.IsDefault,\n\t\t}\n\t\timportCertificateResp, err := d.sdkClient.ImportCertificate(importCertificateReq)\n\t\td.logger.Debug(\"sdk request 'SYNO.Core.Certificate:import'\", slog.Any(\"request\", importCertificateReq), slog.Any(\"response\", importCertificateResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'SYNO.Core.Certificate:import': %w\", err)\n\t\t}\n\t} else {\n\t\t// 查找证书列表，找到已有证书\n\t\tvar certInfo *dsmsdk.CertificateInfo\n\t\tlistCertificatesResp, err := d.sdkClient.ListCertificates()\n\t\td.logger.Debug(\"sdk request 'SYNO.Core.Certificate.CRT:list'\", slog.Any(\"response\", listCertificatesResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'SYNO.Core.Certificate.CRT:list': %w\", err)\n\t\t} else {\n\t\t\tmatchedCerts := lo.Filter(listCertificatesResp.Data.Certificates, func(certItem *dsmsdk.CertificateInfo, _ int) bool {\n\t\t\t\treturn certItem.ID == d.config.CertificateIdOrDescription\n\t\t\t})\n\t\t\tif len(matchedCerts) == 0 {\n\t\t\t\tmatchedCerts = lo.Filter(listCertificatesResp.Data.Certificates, func(certItem *dsmsdk.CertificateInfo, _ int) bool {\n\t\t\t\t\treturn certItem.Description == d.config.CertificateIdOrDescription\n\t\t\t\t})\n\t\t\t}\n\t\t\tif len(matchedCerts) == 0 {\n\t\t\t\treturn nil, fmt.Errorf(\"could not find certificate '%s'\", d.config.CertificateIdOrDescription)\n\t\t\t} else {\n\t\t\t\tif len(matchedCerts) > 1 {\n\t\t\t\t\td.logger.Warn(\"found several certificates matched '%s', using the first one\")\n\t\t\t\t}\n\t\t\t\tcertInfo = matchedCerts[0]\n\t\t\t}\n\t\t}\n\n\t\t// 导入证书\n\t\timportCertificateReq := &dsmsdk.ImportCertificateRequest{\n\t\t\tID:          certInfo.ID,\n\t\t\tDescription: certInfo.Description,\n\t\t\tKey:         privkeyPEM,\n\t\t\tCert:        serverCertPEM,\n\t\t\tInterCert:   intermediateCertPEM,\n\t\t\tAsDefault:   d.config.IsDefault || certInfo.IsDefault,\n\t\t}\n\t\timportCertificateResp, err := d.sdkClient.ImportCertificate(importCertificateReq)\n\t\td.logger.Debug(\"sdk request 'SYNO.Core.Certificate:import'\", slog.Any(\"request\", importCertificateReq), slog.Any(\"response\", importCertificateResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'SYNO.Core.Certificate:import': %w\", err)\n\t\t}\n\t}\n\n\tif d.config.IsDefault {\n\t\t// 查找证书列表，找到默认证书\n\t\tlistCertificatesResp, err := d.sdkClient.ListCertificates()\n\t\td.logger.Debug(\"sdk request 'SYNO.Core.Certificate.CRT:list'\", slog.Any(\"response\", listCertificatesResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'SYNO.Core.Certificate.CRT:list': %w\", err)\n\t\t} else {\n\t\t\tvar defaultCertId string\n\t\t\tfor _, certItem := range listCertificatesResp.Data.Certificates {\n\t\t\t\tif certItem.IsDefault {\n\t\t\t\t\tdefaultCertId = certItem.ID\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif defaultCertId != \"\" {\n\t\t\t\tsettings := make([]*dsmsdk.ServiceCertificateSetting, 0)\n\t\t\t\tfor _, certItem := range listCertificatesResp.Data.Certificates {\n\t\t\t\t\tif certItem.ID == defaultCertId {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tfor _, service := range certItem.Services {\n\t\t\t\t\t\tsettings = append(settings, &dsmsdk.ServiceCertificateSetting{\n\t\t\t\t\t\t\tService:   service,\n\t\t\t\t\t\t\tCertID:    defaultCertId,\n\t\t\t\t\t\t\tOldCertID: certItem.ID,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// 应用到所有服务并重启\n\t\t\t\tif len(settings) > 0 {\n\t\t\t\t\tsetServiceCertificateReq := &dsmsdk.SetServiceCertificateRequest{\n\t\t\t\t\t\tSettings: settings,\n\t\t\t\t\t}\n\t\t\t\t\tsetServiceCertificateResp, err := d.sdkClient.SetServiceCertificate(setServiceCertificateReq)\n\t\t\t\t\td.logger.Debug(\"sdk request 'SYNO.Core.Certificate.Service:set'\", slog.Any(\"request\", setServiceCertificateReq), slog.Any(\"response\", setServiceCertificateResp))\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'SYNO.Core.Certificate.Service:set': %w\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(serverUrl string, skipTlsVerify bool) (*dsmsdk.Client, error) {\n\tclient, err := dsmsdk.NewClient(serverUrl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif skipTlsVerify {\n\t\tclient.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/synologydsm/synologydsm_test.go",
    "content": "package synologydsm_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/synologydsm\"\n)\n\nvar (\n\tfInputCertPath       string\n\tfInputKeyPath        string\n\tfServerUrl           string\n\tfUsername            string\n\tfPassword            string\n\tfTotpSecret          string\n\tfCertificateIdOrDesc string\n\tfIsDefault           bool\n)\n\nfunc init() {\n\targsPrefix := \"SYNOLOGYDSM_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fUsername, argsPrefix+\"USERNAME\", \"\", \"\")\n\tflag.StringVar(&fPassword, argsPrefix+\"PASSWORD\", \"\", \"\")\n\tflag.StringVar(&fTotpSecret, argsPrefix+\"TOTPSECRET\", \"\", \"\")\n\tflag.StringVar(&fCertificateIdOrDesc, argsPrefix+\"CERTIFICATEIDORDESC\", \"\", \"\")\n\tflag.BoolVar(&fIsDefault, argsPrefix+\"ISDEFAULT\", false, \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./synology_dsm_test.go -args \\\n\t--SYNOLOGYDSM_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--SYNOLOGYDSM_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--SYNOLOGYDSM_SERVERURL=\"http://127.0.0.1:5000/\" \\\n\t--SYNOLOGYDSM_USERNAME=\"admin\" \\\n\t--SYNOLOGYDSM_PASSWORD=\"password\" \\\n\t--SYNOLOGYDSM_CERTIFICATEIDORDESC=\"your-certificate-id-or-desc\" \\\n\t--SYNOLOGYDSM_ISDEFAULT=true\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"USERNAME: %v\", fUsername),\n\t\t\tfmt.Sprintf(\"PASSWORD: %v\", fPassword),\n\t\t\tfmt.Sprintf(\"TOTPSECRET: %v\", fTotpSecret),\n\t\t\tfmt.Sprintf(\"CERTIFICATEIDORDESC: %v\", fCertificateIdOrDesc),\n\t\t\tfmt.Sprintf(\"ISDEFAULT: %v\", fIsDefault),\n\t\t}, \"\\n\"))\n\n\t\tdeployer, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tServerUrl:                  fServerUrl,\n\t\t\tUsername:                   fUsername,\n\t\t\tPassword:                   fPassword,\n\t\t\tTotpSecret:                 fTotpSecret,\n\t\t\tAllowInsecureConnections:   true,\n\t\t\tCertificateIdOrDescription: fCertificateIdOrDesc,\n\t\t\tIsDefault:                  fIsDefault,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-cdn/consts.go",
    "content": "package tencentcloudcdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-cdn/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\ttccdn \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n)\n\n// This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/cdn/v20180606/client.go\n// to lightweight the vendor packages in the built binary.\ntype CdnClient struct {\n\tcommon.Client\n}\n\nfunc NewCdnClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *CdnClient, err error) {\n\tclient = &CdnClient{}\n\tclient.Init(region).\n\t\tWithCredential(credential).\n\t\tWithProfile(clientProfile)\n\treturn\n}\n\nfunc (c *CdnClient) DescribeCertDomains(request *tccdn.DescribeCertDomainsRequest) (response *tccdn.DescribeCertDomainsResponse, err error) {\n\treturn c.DescribeCertDomainsWithContext(context.Background(), request)\n}\n\nfunc (c *CdnClient) DescribeCertDomainsWithContext(ctx context.Context, request *tccdn.DescribeCertDomainsRequest) (response *tccdn.DescribeCertDomainsResponse, err error) {\n\tif request == nil {\n\t\trequest = tccdn.NewDescribeCertDomainsRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"cdn\", tccdn.APIVersion, \"DescribeCertDomains\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeCertDomains require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tccdn.NewDescribeCertDomainsResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *CdnClient) DescribeDomains(request *tccdn.DescribeDomainsRequest) (response *tccdn.DescribeDomainsResponse, err error) {\n\treturn c.DescribeDomainsWithContext(context.Background(), request)\n}\n\nfunc (c *CdnClient) DescribeDomainsWithContext(ctx context.Context, request *tccdn.DescribeDomainsRequest) (response *tccdn.DescribeDomainsResponse, err error) {\n\tif request == nil {\n\t\trequest = tccdn.NewDescribeDomainsRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"cdn\", tccdn.APIVersion, \"DescribeDomains\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeDomains require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tccdn.NewDescribeDomainsResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *CdnClient) DescribeDomainsConfig(request *tccdn.DescribeDomainsConfigRequest) (response *tccdn.DescribeDomainsConfigResponse, err error) {\n\treturn c.DescribeDomainsConfigWithContext(context.Background(), request)\n}\n\nfunc (c *CdnClient) DescribeDomainsConfigWithContext(ctx context.Context, request *tccdn.DescribeDomainsConfigRequest) (response *tccdn.DescribeDomainsConfigResponse, err error) {\n\tif request == nil {\n\t\trequest = tccdn.NewDescribeDomainsConfigRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"cdn\", tccdn.APIVersion, \"DescribeDomainsConfig\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeDomainsConfig require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tccdn.NewDescribeDomainsConfigResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *CdnClient) UpdateDomainConfig(request *tccdn.UpdateDomainConfigRequest) (response *tccdn.UpdateDomainConfigResponse, err error) {\n\treturn c.UpdateDomainConfigWithContext(context.Background(), request)\n}\n\nfunc (c *CdnClient) UpdateDomainConfigWithContext(ctx context.Context, request *tccdn.UpdateDomainConfigRequest) (response *tccdn.UpdateDomainConfigResponse, err error) {\n\tif request == nil {\n\t\trequest = tccdn.NewUpdateDomainConfigRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"cdn\", tccdn.APIVersion, \"UpdateDomainConfig\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"UpdateDomainConfig require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tccdn.NewUpdateDomainConfigResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go",
    "content": "package tencentcloudcdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\ttccdn \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-cdn/internal\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 腾讯云 SecretId。\n\tSecretId string `json:\"secretId\"`\n\t// 腾讯云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 腾讯云接口端点。\n\tEndpoint string `json:\"endpoint,omitempty\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.CdnClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tSecretId:  config.SecretId,\n\t\tSecretKey: config.SecretKey,\n\t\tEndpoint: lo.\n\t\t\tIf(strings.HasSuffix(config.Endpoint, \"intl.tencentcloudapi.com\"), \"ssl.intl.tencentcloudapi.com\"). // 国际站使用独立的接口端点\n\t\t\tElse(\"\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的 CDN 实例\n\tdomains := make([]string, 0)\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getMatchedDomainsByWildcard(ctx, d.config.Domain)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = domainCandidates\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tdomainCandidates, err := d.getMatchedDomainsByCertId(ctx, upres.CertId)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = domainCandidates\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no cdn domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found cdn domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getMatchedDomainsByWildcard(ctx context.Context, wildcardDomain string) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名基本信息，获取匹配的域名\n\t// REF: https://cloud.tencent.com/document/api/228/41118\n\tdescribeDomainsOffset := 0\n\tdescribeDomainsLimit := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeDomainsReq := tccdn.NewDescribeDomainsRequest()\n\t\tdescribeDomainsReq.Filters = []*tccdn.DomainFilter{\n\t\t\t{\n\t\t\t\tName:  common.StringPtr(\"domain\"),\n\t\t\t\tValue: common.StringPtrs([]string{strings.TrimPrefix(wildcardDomain, \"*.\")}),\n\t\t\t\tFuzzy: common.BoolPtr(true),\n\t\t\t},\n\t\t}\n\t\tdescribeDomainsReq.Offset = common.Int64Ptr(int64(describeDomainsOffset))\n\t\tdescribeDomainsReq.Limit = common.Int64Ptr(int64(describeDomainsLimit))\n\t\tdescribeDomainsResp, err := d.sdkClient.DescribeDomains(describeDomainsReq)\n\t\td.logger.Debug(\"sdk request 'cdn.DescribeDomains'\", slog.Any(\"request\", describeDomainsReq), slog.Any(\"response\", describeDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.DescribeDomains': %w\", err)\n\t\t}\n\n\t\tif describeDomainsResp.Response == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, domainItem := range describeDomainsResp.Response.Domains {\n\t\t\tif lo.FromPtr(domainItem.Product) == \"cdn\" && xcerthostname.IsMatch(wildcardDomain, lo.FromPtr(domainItem.Domain)) {\n\t\t\t\tdomains = append(domains, lo.FromPtr(domainItem.Domain))\n\t\t\t}\n\t\t}\n\n\t\tif len(describeDomainsResp.Response.Domains) < describeDomainsLimit {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeDomainsOffset += describeDomainsLimit\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) getMatchedDomainsByCertId(ctx context.Context, cloudCertId string) ([]string, error) {\n\t// 获取证书中的可用域名\n\t// REF: https://cloud.tencent.com/document/api/228/42491\n\tdescribeCertDomainsReq := tccdn.NewDescribeCertDomainsRequest()\n\tdescribeCertDomainsReq.CertId = common.StringPtr(cloudCertId)\n\tdescribeCertDomainsReq.Product = common.StringPtr(\"cdn\")\n\tdescribeCertDomainsResp, err := d.sdkClient.DescribeCertDomains(describeCertDomainsReq)\n\td.logger.Debug(\"sdk request 'cdn.DescribeCertDomains'\", slog.Any(\"request\", describeCertDomainsReq), slog.Any(\"response\", describeCertDomainsResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.DescribeCertDomains': %w\", err)\n\t}\n\n\tdomains := make([]string, 0)\n\tif describeCertDomainsResp.Response.Domains != nil {\n\t\tfor _, domain := range describeCertDomainsResp.Response.Domains {\n\t\t\tdomains = append(domains, *domain)\n\t\t}\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error {\n\t// 查询域名详细配置\n\t// REF: https://cloud.tencent.com/document/api/228/41117\n\tdescribeDomainsConfigReq := tccdn.NewDescribeDomainsConfigRequest()\n\tdescribeDomainsConfigReq.Filters = []*tccdn.DomainFilter{\n\t\t{\n\t\t\tName:  common.StringPtr(\"domain\"),\n\t\t\tValue: common.StringPtrs([]string{domain}),\n\t\t},\n\t}\n\tdescribeDomainsConfigReq.Offset = common.Int64Ptr(0)\n\tdescribeDomainsConfigReq.Limit = common.Int64Ptr(1)\n\tdescribeDomainsConfigResp, err := d.sdkClient.DescribeDomainsConfig(describeDomainsConfigReq)\n\td.logger.Debug(\"sdk request 'cdn.DescribeDomainsConfig'\", slog.Any(\"request\", describeDomainsConfigReq), slog.Any(\"response\", describeDomainsConfigResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.DescribeDomainsConfig': %w\", err)\n\t} else if len(describeDomainsConfigResp.Response.Domains) == 0 {\n\t\treturn fmt.Errorf(\"could not find domain '%s'\", domain)\n\t}\n\n\tdomainConfig := describeDomainsConfigResp.Response.Domains[0]\n\tif domainConfig.Https != nil &&\n\t\tdomainConfig.Https.CertInfo != nil &&\n\t\tdomainConfig.Https.CertInfo.CertId != nil &&\n\t\t*domainConfig.Https.CertInfo.CertId == cloudCertId {\n\t\t// 已部署过此域名，跳过\n\t\treturn nil\n\t}\n\n\t// 更新加速域名配置\n\t// REF: https://cloud.tencent.com/document/api/228/41116\n\tupdateDomainConfigReq := tccdn.NewUpdateDomainConfigRequest()\n\tupdateDomainConfigReq.Domain = common.StringPtr(domain)\n\tupdateDomainConfigReq.Https = domainConfig.Https\n\tif updateDomainConfigReq.Https == nil {\n\t\tupdateDomainConfigReq.Https = &tccdn.Https{Switch: common.StringPtr(\"on\")}\n\t} else {\n\t\tupdateDomainConfigReq.Https.SslStatus = nil\n\t}\n\tupdateDomainConfigReq.Https.CertInfo = &tccdn.ServerCert{\n\t\tCertId: common.StringPtr(cloudCertId),\n\t}\n\tupdateDomainConfigResp, err := d.sdkClient.UpdateDomainConfig(updateDomainConfigReq)\n\td.logger.Debug(\"sdk request 'cdn.UpdateDomainConfig'\", slog.Any(\"request\", updateDomainConfigReq), slog.Any(\"response\", updateDomainConfigResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.UpdateDomainConfig': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(secretId, secretKey, endpoint string) (*internal.CdnClient, error) {\n\tcredential := common.NewCredential(secretId, secretKey)\n\n\tcpf := profile.NewClientProfile()\n\tif endpoint != \"\" {\n\t\tcpf.HttpProfile.Endpoint = endpoint\n\t}\n\n\tclient, err := internal.NewCdnClient(credential, \"\", cpf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-cdn/tencentcloud_cdn_test.go",
    "content": "package tencentcloudcdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-cdn\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfSecretId      string\n\tfSecretKey     string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"TENCENTCLOUDCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fSecretId, argsPrefix+\"SECRETID\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./tencentcloud_cdn_test.go -args \\\n\t--TENCENTCLOUDCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--TENCENTCLOUDCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--TENCENTCLOUDCDN_SECRETID=\"your-secret-id\" \\\n\t--TENCENTCLOUDCDN_SECRETKEY=\"your-secret-key\" \\\n\t--TENCENTCLOUDCDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SECRETID: %v\", fSecretId),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tSecretId:           fSecretId,\n\t\t\tSecretKey:          fSecretKey,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-clb/consts.go",
    "content": "package tencentcloudclb\n\nconst (\n\t// 资源类型：部署到指定负载均衡器。\n\tRESOURCE_TYPE_LOADBALANCER = \"loadbalancer\"\n\t// 资源类型：部署到指定监听器。\n\tRESOURCE_TYPE_LISTENER = \"listener\"\n\t// 资源类型：部署到指定转发规则域名。\n\tRESOURCE_TYPE_RULEDOMAIN = \"ruledomain\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-clb/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\ttcclb \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb/v20180317\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n)\n\n// This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/clb/v20180317/client.go\n// to lightweight the vendor packages in the built binary.\ntype ClbClient struct {\n\tcommon.Client\n}\n\nfunc NewClbClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *ClbClient, err error) {\n\tclient = &ClbClient{}\n\tclient.Init(region).\n\t\tWithCredential(credential).\n\t\tWithProfile(clientProfile)\n\treturn\n}\n\nfunc (c *ClbClient) DescribeListeners(request *tcclb.DescribeListenersRequest) (response *tcclb.DescribeListenersResponse, err error) {\n\treturn c.DescribeListenersWithContext(context.Background(), request)\n}\n\nfunc (c *ClbClient) DescribeListenersWithContext(ctx context.Context, request *tcclb.DescribeListenersRequest) (response *tcclb.DescribeListenersResponse, err error) {\n\tif request == nil {\n\t\trequest = tcclb.NewDescribeListenersRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"clb\", tcclb.APIVersion, \"DescribeListeners\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeListeners require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcclb.NewDescribeListenersResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *ClbClient) DescribeTaskStatus(request *tcclb.DescribeTaskStatusRequest) (response *tcclb.DescribeTaskStatusResponse, err error) {\n\treturn c.DescribeTaskStatusWithContext(context.Background(), request)\n}\n\nfunc (c *ClbClient) DescribeTaskStatusWithContext(ctx context.Context, request *tcclb.DescribeTaskStatusRequest) (response *tcclb.DescribeTaskStatusResponse, err error) {\n\tif request == nil {\n\t\trequest = tcclb.NewDescribeTaskStatusRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"clb\", tcclb.APIVersion, \"DescribeTaskStatus\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeTaskStatus require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcclb.NewDescribeTaskStatusResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *ClbClient) ModifyDomainAttributes(request *tcclb.ModifyDomainAttributesRequest) (response *tcclb.ModifyDomainAttributesResponse, err error) {\n\treturn c.ModifyDomainAttributesWithContext(context.Background(), request)\n}\n\nfunc (c *ClbClient) ModifyDomainAttributesWithContext(ctx context.Context, request *tcclb.ModifyDomainAttributesRequest) (response *tcclb.ModifyDomainAttributesResponse, err error) {\n\tif request == nil {\n\t\trequest = tcclb.NewModifyDomainAttributesRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"clb\", tcclb.APIVersion, \"ModifyDomainAttributes\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"ModifyDomainAttributes require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcclb.NewModifyDomainAttributesResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *ClbClient) ModifyListener(request *tcclb.ModifyListenerRequest) (response *tcclb.ModifyListenerResponse, err error) {\n\treturn c.ModifyListenerWithContext(context.Background(), request)\n}\n\nfunc (c *ClbClient) ModifyListenerWithContext(ctx context.Context, request *tcclb.ModifyListenerRequest) (response *tcclb.ModifyListenerResponse, err error) {\n\tif request == nil {\n\t\trequest = tcclb.NewModifyListenerRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"clb\", tcclb.APIVersion, \"ModifyListener\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"ModifyListener require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcclb.NewModifyListenerResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-clb/tencentcloud_clb.go",
    "content": "package tencentcloudclb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\ttcclb \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb/v20180317\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-clb/internal\"\n\txwait \"github.com/certimate-go/certimate/pkg/utils/wait\"\n)\n\ntype DeployerConfig struct {\n\t// 腾讯云 SecretId。\n\tSecretId string `json:\"secretId\"`\n\t// 腾讯云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 腾讯云接口端点。\n\tEndpoint string `json:\"endpoint,omitempty\"`\n\t// 腾讯云地域。\n\tRegion string `json:\"region\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 负载均衡器 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_SSLDEPLOY]、[RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_RULEDOMAIN] 时必填。\n\tLoadbalancerId string `json:\"loadbalancerId,omitempty\"`\n\t// 负载均衡监听 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_SSLDEPLOY]、[RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER]、[RESOURCE_TYPE_RULEDOMAIN] 时必填。\n\tListenerId string `json:\"listenerId,omitempty\"`\n\t// SNI 域名或七层转发规则域名（支持泛域名）。\n\t// 部署资源类型为 [RESOURCE_TYPE_SSLDEPLOY] 时选填；部署资源类型为 [RESOURCE_TYPE_RULEDOMAIN] 时必填。\n\tDomain string `json:\"domain,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.ClbClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tSecretId:  config.SecretId,\n\t\tSecretKey: config.SecretKey,\n\t\tEndpoint: lo.\n\t\t\tIf(strings.HasSuffix(config.Endpoint, \"intl.tencentcloudapi.com\"), \"ssl.intl.tencentcloudapi.com\"). // 国际站使用独立的接口端点\n\t\t\tElse(\"\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_LOADBALANCER:\n\t\tif err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_LISTENER:\n\t\tif err := d.deployToListener(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_RULEDOMAIN:\n\t\tif err := d.deployToRuleDomain(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\n\t// 查询监听器列表\n\t// REF: https://cloud.tencent.com/document/api/214/30686\n\tlistenerIds := make([]string, 0)\n\tdescribeListenersReq := tcclb.NewDescribeListenersRequest()\n\tdescribeListenersReq.LoadBalancerId = common.StringPtr(d.config.LoadbalancerId)\n\tdescribeListenersResp, err := d.sdkClient.DescribeListeners(describeListenersReq)\n\td.logger.Debug(\"sdk request 'clb.DescribeListeners'\", slog.Any(\"request\", describeListenersReq), slog.Any(\"response\", describeListenersResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'clb.DescribeListeners': %w\", err)\n\t} else {\n\t\tif describeListenersResp.Response.Listeners != nil {\n\t\t\tfor _, listener := range describeListenersResp.Response.Listeners {\n\t\t\t\tif listener.Protocol == nil || (*listener.Protocol != \"HTTPS\" && *listener.Protocol != \"TCP_SSL\" && *listener.Protocol != \"QUIC\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tlistenerIds = append(listenerIds, *listener.ListenerId)\n\t\t\t}\n\t\t}\n\t}\n\n\t// 遍历更新监听器证书\n\tif len(listenerIds) == 0 {\n\t\td.logger.Info(\"no clb listeners to deploy\")\n\t} else {\n\t\td.logger.Info(\"found https/tcpssl/quic listeners to deploy\", slog.Any(\"listenerIds\", listenerIds))\n\t\tvar errs []error\n\n\t\tfor _, listenerId := range listenerIds {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listenerId, cloudCertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\tif d.config.ListenerId == \"\" {\n\t\treturn errors.New(\"config `listenerId` is required\")\n\t}\n\n\t// 更新监听器证书\n\tif err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, d.config.ListenerId, cloudCertId); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToRuleDomain(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\tif d.config.ListenerId == \"\" {\n\t\treturn errors.New(\"config `listenerId` is required\")\n\t}\n\tif d.config.Domain == \"\" {\n\t\treturn errors.New(\"config `domain` is required\")\n\t}\n\n\t// 修改负载均衡七层监听器转发规则的域名级别属性\n\t// REF: https://cloud.tencent.com/document/api/214/38092\n\tmodifyDomainAttributesReq := tcclb.NewModifyDomainAttributesRequest()\n\tmodifyDomainAttributesReq.LoadBalancerId = common.StringPtr(d.config.LoadbalancerId)\n\tmodifyDomainAttributesReq.ListenerId = common.StringPtr(d.config.ListenerId)\n\tmodifyDomainAttributesReq.Domain = common.StringPtr(d.config.Domain)\n\tmodifyDomainAttributesReq.Certificate = &tcclb.CertificateInput{\n\t\tSSLMode: common.StringPtr(\"UNIDIRECTIONAL\"),\n\t\tCertId:  common.StringPtr(cloudCertId),\n\t}\n\tmodifyDomainAttributesResp, err := d.sdkClient.ModifyDomainAttributes(modifyDomainAttributesReq)\n\td.logger.Debug(\"sdk request 'clb.ModifyDomainAttributes'\", slog.Any(\"request\", modifyDomainAttributesReq), slog.Any(\"response\", modifyDomainAttributesResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'clb.ModifyDomainAttributes': %w\", err)\n\t}\n\n\t// 查询异步任务状态，等待任务状态变更\n\t// REF: https://cloud.tencent.com/document/product/214/30683\n\tif _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) {\n\t\tdescribeTaskStatusReq := tcclb.NewDescribeTaskStatusRequest()\n\t\tdescribeTaskStatusReq.TaskId = modifyDomainAttributesResp.Response.RequestId\n\t\tdescribeTaskStatusResp, err := d.sdkClient.DescribeTaskStatus(describeTaskStatusReq)\n\t\td.logger.Debug(\"sdk request 'clb.DescribeTaskStatus'\", slog.Any(\"request\", describeTaskStatusReq), slog.Any(\"response\", describeTaskStatusResp))\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to execute sdk request 'clb.DescribeTaskStatus': %w\", err)\n\t\t}\n\n\t\tswitch lo.FromPtr(describeTaskStatusResp.Response.Status) {\n\t\tcase 0:\n\t\t\treturn true, nil\n\t\tcase 1:\n\t\t\treturn false, fmt.Errorf(\"unexpected tencentcloud task status\")\n\t\t}\n\n\t\td.logger.Info(\"waiting for tencentcloud task completion ...\")\n\t\treturn false, nil\n\t}, time.Second*5); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId, cloudListenerId, cloudCertId string) error {\n\t// 查询负载均衡的监听器列表\n\t// REF: https://cloud.tencent.com/document/api/214/30686\n\tdescribeListenersReq := tcclb.NewDescribeListenersRequest()\n\tdescribeListenersReq.LoadBalancerId = common.StringPtr(cloudLoadbalancerId)\n\tdescribeListenersReq.ListenerIds = common.StringPtrs([]string{cloudListenerId})\n\tdescribeListenersResp, err := d.sdkClient.DescribeListeners(describeListenersReq)\n\td.logger.Debug(\"sdk request 'clb.DescribeListeners'\", slog.Any(\"request\", describeListenersReq), slog.Any(\"response\", describeListenersResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'clb.DescribeListeners': %w\", err)\n\t} else if len(describeListenersResp.Response.Listeners) == 0 {\n\t\treturn fmt.Errorf(\"could not find listener '%s'\", cloudListenerId)\n\t}\n\n\t// 修改监听器属性\n\t// REF: https://cloud.tencent.com/document/api/214/30681\n\tmodifyListenerReq := tcclb.NewModifyListenerRequest()\n\tmodifyListenerReq.LoadBalancerId = common.StringPtr(cloudLoadbalancerId)\n\tmodifyListenerReq.ListenerId = common.StringPtr(cloudListenerId)\n\tmodifyListenerReq.Certificate = &tcclb.CertificateInput{CertId: common.StringPtr(cloudCertId)}\n\tif describeListenersResp.Response.Listeners[0].Certificate != nil && describeListenersResp.Response.Listeners[0].Certificate.SSLMode != nil {\n\t\tmodifyListenerReq.Certificate.SSLMode = describeListenersResp.Response.Listeners[0].Certificate.SSLMode\n\t\tmodifyListenerReq.Certificate.CertCaId = describeListenersResp.Response.Listeners[0].Certificate.CertCaId\n\t} else {\n\t\tmodifyListenerReq.Certificate.SSLMode = common.StringPtr(\"UNIDIRECTIONAL\")\n\t}\n\tmodifyListenerResp, err := d.sdkClient.ModifyListener(modifyListenerReq)\n\td.logger.Debug(\"sdk request 'clb.ModifyListener'\", slog.Any(\"request\", modifyListenerReq), slog.Any(\"response\", modifyListenerResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'clb.ModifyListener': %w\", err)\n\t}\n\n\t// 查询异步任务状态，等待任务状态变更\n\t// REF: https://cloud.tencent.com/document/product/214/30683\n\tif _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) {\n\t\tdescribeTaskStatusReq := tcclb.NewDescribeTaskStatusRequest()\n\t\tdescribeTaskStatusReq.TaskId = modifyListenerResp.Response.RequestId\n\t\tdescribeTaskStatusResp, err := d.sdkClient.DescribeTaskStatus(describeTaskStatusReq)\n\t\td.logger.Debug(\"sdk request 'clb.DescribeTaskStatus'\", slog.Any(\"request\", describeTaskStatusReq), slog.Any(\"response\", describeTaskStatusResp))\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to execute sdk request 'clb.DescribeTaskStatus': %w\", err)\n\t\t}\n\n\t\tswitch lo.FromPtr(describeTaskStatusResp.Response.Status) {\n\t\tcase 0:\n\t\t\treturn true, nil\n\t\tcase 1:\n\t\t\treturn false, fmt.Errorf(\"unexpected tencentcloud task status\")\n\t\t}\n\n\t\td.logger.Info(\"waiting for tencentcloud task completion ...\")\n\t\treturn false, nil\n\t}, time.Second*5); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(secretId, secretKey, endpoint, region string) (*internal.ClbClient, error) {\n\tcredential := common.NewCredential(secretId, secretKey)\n\n\tcpf := profile.NewClientProfile()\n\tif endpoint != \"\" {\n\t\tcpf.HttpProfile.Endpoint = endpoint\n\t}\n\n\tclient, err := internal.NewClbClient(credential, region, cpf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-clb/tencentcloud_clb_test.go",
    "content": "package tencentcloudclb_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-clb\"\n)\n\nvar (\n\tfInputCertPath  string\n\tfInputKeyPath   string\n\tfSecretId       string\n\tfSecretKey      string\n\tfRegion         string\n\tfLoadbalancerId string\n\tfListenerId     string\n\tfDomain         string\n)\n\nfunc init() {\n\targsPrefix := \"TENCENTCLOUDCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fSecretId, argsPrefix+\"SECRETID\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fLoadbalancerId, argsPrefix+\"LOADBALANCERID\", \"\", \"\")\n\tflag.StringVar(&fListenerId, argsPrefix+\"LISTENERID\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./tencentcloud_clb_test.go -args \\\n\t--TENCENTCLOUDCLB_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--TENCENTCLOUDCLB_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--TENCENTCLOUDCLB_SECRETID=\"your-secret-id\" \\\n\t--TENCENTCLOUDCLB_SECRETKEY=\"your-secret-key\" \\\n\t--TENCENTCLOUDCLB_REGION=\"ap-guangzhou\" \\\n\t--TENCENTCLOUDCLB_LOADBALANCERID=\"your-clb-lb-id\" \\\n\t--TENCENTCLOUDCLB_LISTENERID=\"your-clb-lbl-id\" \\\n\t--TENCENTCLOUDCLB_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy_ToLoadbalancer\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SECRETID: %v\", fSecretId),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LOADBALANCERID: %v\", fLoadbalancerId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tSecretId:       fSecretId,\n\t\t\tSecretKey:      fSecretKey,\n\t\t\tRegion:         fRegion,\n\t\t\tResourceType:   provider.RESOURCE_TYPE_LOADBALANCER,\n\t\t\tLoadbalancerId: fLoadbalancerId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n\n\tt.Run(\"Deploy_ToListener\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SECRETID: %v\", fSecretId),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LOADBALANCERID: %v\", fLoadbalancerId),\n\t\t\tfmt.Sprintf(\"LISTENERID: %v\", fListenerId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tSecretId:       fSecretId,\n\t\t\tSecretKey:      fSecretKey,\n\t\t\tRegion:         fRegion,\n\t\t\tResourceType:   provider.RESOURCE_TYPE_LISTENER,\n\t\t\tLoadbalancerId: fLoadbalancerId,\n\t\t\tListenerId:     fListenerId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n\n\tt.Run(\"Deploy_ToRuleDomain\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SECRETID: %v\", fSecretId),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LOADBALANCERID: %v\", fLoadbalancerId),\n\t\t\tfmt.Sprintf(\"LISTENERID: %v\", fListenerId),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tSecretId:       fSecretId,\n\t\t\tSecretKey:      fSecretKey,\n\t\t\tRegion:         fRegion,\n\t\t\tResourceType:   provider.RESOURCE_TYPE_RULEDOMAIN,\n\t\t\tLoadbalancerId: fLoadbalancerId,\n\t\t\tListenerId:     fListenerId,\n\t\t\tDomain:         fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-cos/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcssl \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205\"\n)\n\n// This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/ssl/v20191205/client.go\n// to lightweight the vendor packages in the built binary.\ntype SslClient struct {\n\tcommon.Client\n}\n\nfunc NewSslClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *SslClient, err error) {\n\tclient = &SslClient{}\n\tclient.Init(region).\n\t\tWithCredential(credential).\n\t\tWithProfile(clientProfile)\n\treturn\n}\n\nfunc (c *SslClient) DescribeHostCosInstanceList(request *tcssl.DescribeHostCosInstanceListRequest) (response *tcssl.DescribeHostCosInstanceListResponse, err error) {\n\treturn c.DescribeHostCosInstanceListWithContext(context.Background(), request)\n}\n\nfunc (c *SslClient) DescribeHostCosInstanceListWithContext(ctx context.Context, request *tcssl.DescribeHostCosInstanceListRequest) (response *tcssl.DescribeHostCosInstanceListResponse, err error) {\n\tif request == nil {\n\t\trequest = tcssl.NewDescribeHostCosInstanceListRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"ssl\", tcssl.APIVersion, \"DescribeHostCosInstanceList\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeHostCosInstanceList require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcssl.NewDescribeHostCosInstanceListResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *SslClient) DescribeHostDeployRecordDetail(request *tcssl.DescribeHostDeployRecordDetailRequest) (response *tcssl.DescribeHostDeployRecordDetailResponse, err error) {\n\treturn c.DescribeHostDeployRecordDetailWithContext(context.Background(), request)\n}\n\nfunc (c *SslClient) DescribeHostDeployRecordDetailWithContext(ctx context.Context, request *tcssl.DescribeHostDeployRecordDetailRequest) (response *tcssl.DescribeHostDeployRecordDetailResponse, err error) {\n\tif request == nil {\n\t\trequest = tcssl.NewDescribeHostDeployRecordDetailRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"ssl\", tcssl.APIVersion, \"DescribeHostDeployRecordDetail\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeHostDeployRecordDetail require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcssl.NewDescribeHostDeployRecordDetailResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *SslClient) DeployCertificateInstance(request *tcssl.DeployCertificateInstanceRequest) (response *tcssl.DeployCertificateInstanceResponse, err error) {\n\treturn c.DeployCertificateInstanceWithContext(context.Background(), request)\n}\n\nfunc (c *SslClient) DeployCertificateInstanceWithContext(ctx context.Context, request *tcssl.DeployCertificateInstanceRequest) (response *tcssl.DeployCertificateInstanceResponse, err error) {\n\tif request == nil {\n\t\trequest = tcssl.NewDeployCertificateInstanceRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"ssl\", tcssl.APIVersion, \"DeployCertificateInstance\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DeployCertificateInstance require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcssl.NewDeployCertificateInstanceResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-cos/tencentcloud_cos.go",
    "content": "package tencentcloudcos\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcssl \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-cos/internal\"\n\txwait \"github.com/certimate-go/certimate/pkg/utils/wait\"\n)\n\ntype DeployerConfig struct {\n\t// 腾讯云 SecretId。\n\tSecretId string `json:\"secretId\"`\n\t// 腾讯云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 腾讯云地域。\n\tRegion string `json:\"region\"`\n\t// 存储桶名。\n\tBucket string `json:\"bucket\"`\n\t// 自定义域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *wSDKClients\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\ntype wSDKClients struct {\n\tSSL *internal.SslClient\n}\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclients, err := createSDKClients(config.SecretId, config.SecretKey, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tSecretId:  config.SecretId,\n\t\tSecretKey: config.SecretKey,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  clients,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.Bucket == \"\" {\n\t\treturn nil, errors.New(\"config `bucket` is required\")\n\t}\n\tif d.config.Domain == \"\" {\n\t\treturn nil, errors.New(\"config `domain` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 避免多次部署，否则会报错 https://github.com/certimate-go/certimate/issues/897#issuecomment-3182904098\n\tif bind, _ := d.checkIsBind(ctx, upres.CertId); bind {\n\t\td.logger.Info(\"ssl certificate already deployed\")\n\t\treturn &deployer.DeployResult{}, nil\n\t}\n\n\t// 证书部署到 COS 实例\n\t// REF: https://cloud.tencent.com/document/api/400/91667\n\tdeployCertificateInstanceReq := tcssl.NewDeployCertificateInstanceRequest()\n\tdeployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)\n\tdeployCertificateInstanceReq.ResourceType = common.StringPtr(\"cos\")\n\tdeployCertificateInstanceReq.Status = common.Int64Ptr(1)\n\tdeployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf(\"%s|%s|%s\", d.config.Region, d.config.Bucket, d.config.Domain)})\n\tdeployCertificateInstanceResp, err := d.sdkClient.SSL.DeployCertificateInstance(deployCertificateInstanceReq)\n\td.logger.Debug(\"sdk request 'ssl.DeployCertificateInstance'\", slog.Any(\"request\", deployCertificateInstanceReq), slog.Any(\"response\", deployCertificateInstanceResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'ssl.DeployCertificateInstance': %w\", err)\n\t}\n\n\t// 获取部署任务详情，等待任务状态变更\n\t// REF: https://cloud.tencent.com/document/api/400/91658\n\tif _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) {\n\t\tdescribeHostDeployRecordDetailReq := tcssl.NewDescribeHostDeployRecordDetailRequest()\n\t\tdescribeHostDeployRecordDetailReq.DeployRecordId = common.StringPtr(fmt.Sprintf(\"%d\", *deployCertificateInstanceResp.Response.DeployRecordId))\n\t\tdescribeHostDeployRecordDetailResp, err := d.sdkClient.SSL.DescribeHostDeployRecordDetail(describeHostDeployRecordDetailReq)\n\t\td.logger.Debug(\"sdk request 'ssl.DescribeHostDeployRecordDetail'\", slog.Any(\"request\", describeHostDeployRecordDetailReq), slog.Any(\"response\", describeHostDeployRecordDetailResp))\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to execute sdk request 'ssl.DescribeHostDeployRecordDetail': %w\", err)\n\t\t}\n\n\t\tvar pendingCount, runningCount, succeededCount, failedCount, totalCount int64\n\t\tif describeHostDeployRecordDetailResp.Response.TotalCount == nil {\n\t\t\treturn false, fmt.Errorf(\"unexpected tencentcloud deployment job status\")\n\t\t} else {\n\t\t\tpendingCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.PendingTotalCount)\n\t\t\trunningCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.RunningTotalCount)\n\t\t\tsucceededCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.SuccessTotalCount)\n\t\t\tfailedCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.FailedTotalCount)\n\t\t\ttotalCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.TotalCount)\n\n\t\t\tif succeededCount+failedCount == totalCount {\n\t\t\t\tif failedCount > 0 {\n\t\t\t\t\treturn false, fmt.Errorf(\"tencentcloud deployment job failed (succeeded: %d, failed: %d, total: %d)\", succeededCount, failedCount, totalCount)\n\t\t\t\t}\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t}\n\n\t\td.logger.Info(fmt.Sprintf(\"waiting for tencentcloud deployment job completion (pending: %d, running: %d, succeeded: %d, failed: %d, total: %d) ...\", pendingCount, runningCount, succeededCount, failedCount, totalCount))\n\t\treturn false, nil\n\t}, time.Second*5); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) checkIsBind(ctx context.Context, cloudCertId string) (bool, error) {\n\t// 查询证书 COS 云资源部署实例列表\n\t// REF: https://cloud.tencent.com/document/api/400/91661\n\tdescribeHostCosInstanceListLimit := 100\n\tdescribeHostCosInstanceListOffset := 0\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn false, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeHostCosInstanceListReq := tcssl.NewDescribeHostCosInstanceListRequest()\n\t\tdescribeHostCosInstanceListReq.OldCertificateId = common.StringPtr(cloudCertId)\n\t\tdescribeHostCosInstanceListReq.ResourceType = common.StringPtr(\"cos\")\n\t\tdescribeHostCosInstanceListReq.IsCache = common.Uint64Ptr(0)\n\t\tdescribeHostCosInstanceListReq.Offset = common.Int64Ptr(int64(describeHostCosInstanceListOffset))\n\t\tdescribeHostCosInstanceListReq.Limit = common.Int64Ptr(int64(describeHostCosInstanceListLimit))\n\t\tdescribeHostCosInstanceListResp, err := d.sdkClient.SSL.DescribeHostCosInstanceList(describeHostCosInstanceListReq)\n\t\td.logger.Debug(\"sdk request 'ssl.DescribeHostCosInstanceList'\", slog.Any(\"request\", describeHostCosInstanceListReq), slog.Any(\"response\", describeHostCosInstanceListResp))\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to execute sdk request 'ssl.DescribeHostCosInstanceList': %w\", err)\n\t\t}\n\n\t\tif describeHostCosInstanceListResp.Response == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, instance := range describeHostCosInstanceListResp.Response.InstanceList {\n\t\t\tif lo.FromPtr(instance.Bucket) != d.config.Bucket {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif lo.FromPtr(instance.Domain) != d.config.Domain {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif lo.FromPtr(instance.Status) != \"ENABLED\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn true, nil\n\t\t}\n\n\t\tif len(describeHostCosInstanceListResp.Response.InstanceList) < describeHostCosInstanceListLimit {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeHostCosInstanceListOffset += describeHostCosInstanceListLimit\n\t}\n\n\treturn false, nil\n}\n\nfunc createSDKClients(secretId, secretKey, region string) (*wSDKClients, error) {\n\tcredential := common.NewCredential(secretId, secretKey)\n\tclient, err := internal.NewSslClient(credential, region, profile.NewClientProfile())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &wSDKClients{\n\t\tSSL: client,\n\t}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-cos/tencentcloud_cos_test.go",
    "content": "package tencentcloudcos_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-cos\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfSecretId      string\n\tfSecretKey     string\n\tfRegion        string\n\tfBucket        string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"TENCENTCLOUDCOS_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fSecretId, argsPrefix+\"SECRETID\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fBucket, argsPrefix+\"BUCKET\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./tencentcloud_cos_test.go -args \\\n\t--TENCENTCLOUDCOS_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--TENCENTCLOUDCOS_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--TENCENTCLOUDCOS_SECRETID=\"your-secret-id\" \\\n\t--TENCENTCLOUDCOS_SECRETKEY=\"your-secret-key\" \\\n\t--TENCENTCLOUDCOS_REGION=\"ap-guangzhou\" \\\n\t--TENCENTCLOUDCOS_BUCKET=\"your-cos-bucket\" \\\n\t--TENCENTCLOUDCOS_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SECRETID: %v\", fSecretId),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"BUCKET: %v\", fBucket),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tSecretId:  fSecretId,\n\t\t\tSecretKey: fSecretKey,\n\t\t\tRegion:    fRegion,\n\t\t\tBucket:    fBucket,\n\t\t\tDomain:    fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-css/consts.go",
    "content": "package tencentcloudcss\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-css/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttclive \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live/v20180801\"\n)\n\n// This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/live/v20180801/client.go\n// to lightweight the vendor packages in the built binary.\ntype LiveClient struct {\n\tcommon.Client\n}\n\nfunc NewLiveClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *LiveClient, err error) {\n\tclient = &LiveClient{}\n\tclient.Init(region).\n\t\tWithCredential(credential).\n\t\tWithProfile(clientProfile)\n\treturn\n}\n\nfunc (c *LiveClient) DescribeLiveDomains(request *tclive.DescribeLiveDomainsRequest) (response *tclive.DescribeLiveDomainsResponse, err error) {\n\treturn c.DescribeLiveDomainsWithContext(context.Background(), request)\n}\n\nfunc (c *LiveClient) DescribeLiveDomainsWithContext(ctx context.Context, request *tclive.DescribeLiveDomainsRequest) (response *tclive.DescribeLiveDomainsResponse, err error) {\n\tif request == nil {\n\t\trequest = tclive.NewDescribeLiveDomainsRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"live\", tclive.APIVersion, \"DescribeLiveDomains\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeLiveDomains require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\n\tresponse = tclive.NewDescribeLiveDomainsResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *LiveClient) ModifyLiveDomainCertBindings(request *tclive.ModifyLiveDomainCertBindingsRequest) (response *tclive.ModifyLiveDomainCertBindingsResponse, err error) {\n\treturn c.ModifyLiveDomainCertBindingsWithContext(context.Background(), request)\n}\n\nfunc (c *LiveClient) ModifyLiveDomainCertBindingsWithContext(ctx context.Context, request *tclive.ModifyLiveDomainCertBindingsRequest) (response *tclive.ModifyLiveDomainCertBindingsResponse, err error) {\n\tif request == nil {\n\t\trequest = tclive.NewModifyLiveDomainCertBindingsRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"live\", tclive.APIVersion, \"ModifyLiveDomainCertBindings\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"ModifyLiveDomainCertBindings require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tclive.NewModifyLiveDomainCertBindingsResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-css/tencentcloud_css.go",
    "content": "package tencentcloudcss\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttclive \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live/v20180801\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-css/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype DeployerConfig struct {\n\t// 腾讯云 SecretId。\n\tSecretId string `json:\"secretId\"`\n\t// 腾讯云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 腾讯云接口端点。\n\tEndpoint string `json:\"endpoint,omitempty\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 直播播放域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.LiveClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tSecretId:  config.SecretId,\n\t\tSecretKey: config.SecretKey,\n\t\tEndpoint: lo.\n\t\t\tIf(strings.HasSuffix(config.Endpoint, \"intl.tencentcloudapi.com\"), \"ssl.intl.tencentcloudapi.com\"). // 国际站使用独立的接口端点\n\t\t\tElse(\"\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 批量绑定证书对应的播放域名\n\t// REF: https://cloud.tencent.com/document/api/267/78655\n\tmodifyLiveDomainCertBindingsReq := tclive.NewModifyLiveDomainCertBindingsRequest()\n\tmodifyLiveDomainCertBindingsReq.DomainInfos = lo.Map(domains, func(domain string, _ int) *tclive.LiveCertDomainInfo {\n\t\treturn &tclive.LiveCertDomainInfo{\n\t\t\tDomainName: common.StringPtr(domain),\n\t\t\tStatus:     common.Int64Ptr(1),\n\t\t}\n\t})\n\tmodifyLiveDomainCertBindingsReq.CloudCertId = common.StringPtr(upres.CertId)\n\tmodifyLiveDomainCertBindingsResp, err := d.sdkClient.ModifyLiveDomainCertBindings(modifyLiveDomainCertBindingsReq)\n\td.logger.Debug(\"sdk request 'live.ModifyLiveDomainCertBindings'\", slog.Any(\"request\", modifyLiveDomainCertBindingsReq), slog.Any(\"response\", modifyLiveDomainCertBindingsResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'live.ModifyLiveDomainCertBindings': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// REF: https://cloud.tencent.com/document/api/267/33856\n\tdescribeLiveDomainsPageNum := 1\n\tdescribeLiveDomainsPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeLiveDomainsReq := tclive.NewDescribeLiveDomainsRequest()\n\t\tdescribeLiveDomainsReq.DomainStatus = common.Uint64Ptr(1)\n\t\tdescribeLiveDomainsReq.DomainType = common.Uint64Ptr(1)\n\t\tdescribeLiveDomainsReq.PageNum = common.Uint64Ptr(uint64(describeLiveDomainsPageNum))\n\t\tdescribeLiveDomainsReq.PageSize = common.Uint64Ptr(uint64(describeLiveDomainsPageSize))\n\t\tdescribeLiveDomainsResp, err := d.sdkClient.DescribeLiveDomains(describeLiveDomainsReq)\n\t\td.logger.Debug(\"sdk request 'live.DescribeLiveDomains'\", slog.Any(\"request\", describeLiveDomainsReq), slog.Any(\"response\", describeLiveDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'live.DescribeLiveDomains': %w\", err)\n\t\t}\n\n\t\tif describeLiveDomainsResp.Response == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, domainItem := range describeLiveDomainsResp.Response.DomainList {\n\t\t\tdomains = append(domains, *domainItem.Name)\n\t\t}\n\n\t\tif len(describeLiveDomainsResp.Response.DomainList) < describeLiveDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeLiveDomainsPageNum++\n\t}\n\n\treturn domains, nil\n}\n\nfunc createSDKClient(secretId, secretKey, endpoint string) (*internal.LiveClient, error) {\n\tcredential := common.NewCredential(secretId, secretKey)\n\n\tcpf := profile.NewClientProfile()\n\tif endpoint != \"\" {\n\t\tcpf.HttpProfile.Endpoint = endpoint\n\t}\n\n\tclient, err := internal.NewLiveClient(credential, \"\", cpf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-css/tencentcloud_css_test.go",
    "content": "package tencentcloudcss_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-css\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfSecretId      string\n\tfSecretKey     string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"TENCENTCLOUDCSS_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fSecretId, argsPrefix+\"SECRETID\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./tencentcloud_css_test.go -args \\\n\t--TENCENTCLOUDCSS_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--TENCENTCLOUDCSS_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--TENCENTCLOUDCSS_SECRETID=\"your-secret-id\" \\\n\t--TENCENTCLOUDCSS_SECRETKEY=\"your-secret-key\" \\\n\t--TENCENTCLOUDCSS_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SECRETID: %v\", fSecretId),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tSecretId:  fSecretId,\n\t\t\tSecretKey: fSecretKey,\n\t\t\tDomain:    fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-ecdn/consts.go",
    "content": "package tencentcloudecdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-ecdn/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\ttccdn \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n)\n\n// This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/cdn/v20180606/client.go\n// to lightweight the vendor packages in the built binary.\ntype CdnClient struct {\n\tcommon.Client\n}\n\nfunc NewCdnClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *CdnClient, err error) {\n\tclient = &CdnClient{}\n\tclient.Init(region).\n\t\tWithCredential(credential).\n\t\tWithProfile(clientProfile)\n\treturn\n}\n\nfunc (c *CdnClient) DescribeCertDomains(request *tccdn.DescribeCertDomainsRequest) (response *tccdn.DescribeCertDomainsResponse, err error) {\n\treturn c.DescribeCertDomainsWithContext(context.Background(), request)\n}\n\nfunc (c *CdnClient) DescribeCertDomainsWithContext(ctx context.Context, request *tccdn.DescribeCertDomainsRequest) (response *tccdn.DescribeCertDomainsResponse, err error) {\n\tif request == nil {\n\t\trequest = tccdn.NewDescribeCertDomainsRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"cdn\", tccdn.APIVersion, \"DescribeCertDomains\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeCertDomains require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tccdn.NewDescribeCertDomainsResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *CdnClient) DescribeDomains(request *tccdn.DescribeDomainsRequest) (response *tccdn.DescribeDomainsResponse, err error) {\n\treturn c.DescribeDomainsWithContext(context.Background(), request)\n}\n\nfunc (c *CdnClient) DescribeDomainsWithContext(ctx context.Context, request *tccdn.DescribeDomainsRequest) (response *tccdn.DescribeDomainsResponse, err error) {\n\tif request == nil {\n\t\trequest = tccdn.NewDescribeDomainsRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"cdn\", tccdn.APIVersion, \"DescribeDomains\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeDomains require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tccdn.NewDescribeDomainsResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *CdnClient) DescribeDomainsConfig(request *tccdn.DescribeDomainsConfigRequest) (response *tccdn.DescribeDomainsConfigResponse, err error) {\n\treturn c.DescribeDomainsConfigWithContext(context.Background(), request)\n}\n\nfunc (c *CdnClient) DescribeDomainsConfigWithContext(ctx context.Context, request *tccdn.DescribeDomainsConfigRequest) (response *tccdn.DescribeDomainsConfigResponse, err error) {\n\tif request == nil {\n\t\trequest = tccdn.NewDescribeDomainsConfigRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"cdn\", tccdn.APIVersion, \"DescribeDomainsConfig\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeDomainsConfig require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tccdn.NewDescribeDomainsConfigResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *CdnClient) UpdateDomainConfig(request *tccdn.UpdateDomainConfigRequest) (response *tccdn.UpdateDomainConfigResponse, err error) {\n\treturn c.UpdateDomainConfigWithContext(context.Background(), request)\n}\n\nfunc (c *CdnClient) UpdateDomainConfigWithContext(ctx context.Context, request *tccdn.UpdateDomainConfigRequest) (response *tccdn.UpdateDomainConfigResponse, err error) {\n\tif request == nil {\n\t\trequest = tccdn.NewUpdateDomainConfigRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"cdn\", tccdn.APIVersion, \"UpdateDomainConfig\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"UpdateDomainConfig require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tccdn.NewUpdateDomainConfigResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go",
    "content": "package tencentcloudecdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\ttccdn \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ecdn/internal\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 腾讯云 SecretId。\n\tSecretId string `json:\"secretId\"`\n\t// 腾讯云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 腾讯云接口端点。\n\tEndpoint string `json:\"endpoint,omitempty\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.CdnClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tSecretId:  config.SecretId,\n\t\tSecretKey: config.SecretKey,\n\t\tEndpoint: lo.\n\t\t\tIf(strings.HasSuffix(config.Endpoint, \"intl.tencentcloudapi.com\"), \"ssl.intl.tencentcloudapi.com\"). // 国际站使用独立的接口端点\n\t\t\tElse(\"\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的 ECDN 实例\n\tdomains := make([]string, 0)\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getMatchedDomainsByWildcard(ctx, d.config.Domain)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = domainCandidates\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tdomainCandidates, err := d.getMatchedDomainsByCertId(ctx, upres.CertId)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = domainCandidates\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no ecdn domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found ecdn domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getMatchedDomainsByWildcard(ctx context.Context, wildcardDomain string) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名基本信息，获取匹配的域名\n\t// REF: https://cloud.tencent.com/document/api/228/41118\n\tdescribeDomainsOffset := 0\n\tdescribeDomainsLimit := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeDomainsReq := tccdn.NewDescribeDomainsRequest()\n\t\tdescribeDomainsReq.Filters = []*tccdn.DomainFilter{\n\t\t\t{\n\t\t\t\tName:  common.StringPtr(\"domain\"),\n\t\t\t\tValue: common.StringPtrs([]string{strings.TrimPrefix(wildcardDomain, \"*.\")}),\n\t\t\t\tFuzzy: common.BoolPtr(true),\n\t\t\t},\n\t\t}\n\t\tdescribeDomainsReq.Offset = common.Int64Ptr(int64(describeDomainsOffset))\n\t\tdescribeDomainsReq.Limit = common.Int64Ptr(int64(describeDomainsLimit))\n\t\tdescribeDomainsResp, err := d.sdkClient.DescribeDomains(describeDomainsReq)\n\t\td.logger.Debug(\"sdk request 'cdn.DescribeDomains'\", slog.Any(\"request\", describeDomainsReq), slog.Any(\"response\", describeDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.DescribeDomains': %w\", err)\n\t\t}\n\n\t\tif describeDomainsResp.Response == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, domainItem := range describeDomainsResp.Response.Domains {\n\t\t\tif lo.FromPtr(domainItem.Product) == \"ecdn\" && xcerthostname.IsMatch(wildcardDomain, lo.FromPtr(domainItem.Domain)) {\n\t\t\t\tdomains = append(domains, *domainItem.Domain)\n\t\t\t}\n\t\t}\n\n\t\tif len(describeDomainsResp.Response.Domains) < describeDomainsLimit {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeDomainsOffset += describeDomainsLimit\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) getMatchedDomainsByCertId(ctx context.Context, cloudCertId string) ([]string, error) {\n\t// 获取证书中的可用域名\n\t// REF: https://cloud.tencent.com/document/api/228/42491\n\tdescribeCertDomainsReq := tccdn.NewDescribeCertDomainsRequest()\n\tdescribeCertDomainsReq.CertId = common.StringPtr(cloudCertId)\n\tdescribeCertDomainsReq.Product = common.StringPtr(\"ecdn\")\n\tdescribeCertDomainsResp, err := d.sdkClient.DescribeCertDomains(describeCertDomainsReq)\n\td.logger.Debug(\"sdk request 'cdn.DescribeCertDomains'\", slog.Any(\"request\", describeCertDomainsReq), slog.Any(\"response\", describeCertDomainsResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.DescribeCertDomains': %w\", err)\n\t}\n\n\tdomains := make([]string, 0)\n\tif describeCertDomainsResp.Response.Domains != nil {\n\t\tfor _, domain := range describeCertDomainsResp.Response.Domains {\n\t\t\tdomains = append(domains, *domain)\n\t\t}\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error {\n\t// 查询域名详细配置\n\t// REF: https://cloud.tencent.com/document/api/228/41117\n\tdescribeDomainsConfigReq := tccdn.NewDescribeDomainsConfigRequest()\n\tdescribeDomainsConfigReq.Filters = []*tccdn.DomainFilter{\n\t\t{\n\t\t\tName:  common.StringPtr(\"domain\"),\n\t\t\tValue: common.StringPtrs([]string{domain}),\n\t\t},\n\t}\n\tdescribeDomainsConfigReq.Offset = common.Int64Ptr(0)\n\tdescribeDomainsConfigReq.Limit = common.Int64Ptr(1)\n\tdescribeDomainsConfigResp, err := d.sdkClient.DescribeDomainsConfig(describeDomainsConfigReq)\n\td.logger.Debug(\"sdk request 'cdn.DescribeDomainsConfig'\", slog.Any(\"request\", describeDomainsConfigReq), slog.Any(\"response\", describeDomainsConfigResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.DescribeDomainsConfig': %w\", err)\n\t} else if len(describeDomainsConfigResp.Response.Domains) == 0 {\n\t\treturn fmt.Errorf(\"could not find domain '%s'\", domain)\n\t}\n\n\tdomainConfig := describeDomainsConfigResp.Response.Domains[0]\n\tif domainConfig.Https != nil && domainConfig.Https.CertInfo != nil && domainConfig.Https.CertInfo.CertId != nil && *domainConfig.Https.CertInfo.CertId == cloudCertId {\n\t\t// 已部署过此域名，跳过\n\t\treturn nil\n\t}\n\n\t// 更新加速域名配置\n\t// REF: https://cloud.tencent.com/document/api/228/41116\n\tupdateDomainConfigReq := tccdn.NewUpdateDomainConfigRequest()\n\tupdateDomainConfigReq.Domain = common.StringPtr(domain)\n\tupdateDomainConfigReq.Https = domainConfig.Https\n\tif updateDomainConfigReq.Https == nil {\n\t\tupdateDomainConfigReq.Https = &tccdn.Https{\n\t\t\tSwitch: common.StringPtr(\"on\"),\n\t\t}\n\t} else {\n\t\tupdateDomainConfigReq.Https.SslStatus = nil\n\t}\n\tupdateDomainConfigReq.Https.CertInfo = &tccdn.ServerCert{\n\t\tCertId: common.StringPtr(cloudCertId),\n\t}\n\tupdateDomainConfigResp, err := d.sdkClient.UpdateDomainConfig(updateDomainConfigReq)\n\td.logger.Debug(\"sdk request 'cdn.UpdateDomainConfig'\", slog.Any(\"request\", updateDomainConfigReq), slog.Any(\"response\", updateDomainConfigResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'cdn.UpdateDomainConfig': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(secretId, secretKey, endpoint string) (*internal.CdnClient, error) {\n\tcredential := common.NewCredential(secretId, secretKey)\n\n\tcpf := profile.NewClientProfile()\n\tif endpoint != \"\" {\n\t\tcpf.HttpProfile.Endpoint = endpoint\n\t}\n\n\tclient, err := internal.NewCdnClient(credential, \"\", cpf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn_test.go",
    "content": "package tencentcloudecdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ecdn\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfSecretId      string\n\tfSecretKey     string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"TENCENTCLOUDECDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fSecretId, argsPrefix+\"SECRETID\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./tencentcloud_ecdn_test.go -args \\\n\t--TENCENTCLOUDECDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--TENCENTCLOUDECDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--TENCENTCLOUDECDN_SECRETID=\"your-secret-id\" \\\n\t--TENCENTCLOUDECDN_SECRETKEY=\"your-secret-key\" \\\n\t--TENCENTCLOUDECDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SECRETID: %v\", fSecretId),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tSecretId:           fSecretId,\n\t\t\tSecretKey:          fSecretKey,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-eo/consts.go",
    "content": "package tencentcloudeo\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-eo/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcteo \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901\"\n)\n\n// This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/teo/v20220901/client.go\n// to lightweight the vendor packages in the built binary.\ntype TeoClient struct {\n\tcommon.Client\n}\n\nfunc NewTeoClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *TeoClient, err error) {\n\tclient = &TeoClient{}\n\tclient.Init(region).\n\t\tWithCredential(credential).\n\t\tWithProfile(clientProfile)\n\treturn\n}\n\nfunc (c *TeoClient) DescribeAccelerationDomains(request *tcteo.DescribeAccelerationDomainsRequest) (response *tcteo.DescribeAccelerationDomainsResponse, err error) {\n\treturn c.DescribeAccelerationDomainsWithContext(context.Background(), request)\n}\n\nfunc (c *TeoClient) DescribeAccelerationDomainsWithContext(ctx context.Context, request *tcteo.DescribeAccelerationDomainsRequest) (response *tcteo.DescribeAccelerationDomainsResponse, err error) {\n\tif request == nil {\n\t\trequest = tcteo.NewDescribeAccelerationDomainsRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"teo\", tcteo.APIVersion, \"DescribeAccelerationDomains\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeAccelerationDomains require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcteo.NewDescribeAccelerationDomainsResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *TeoClient) ModifyHostsCertificate(request *tcteo.ModifyHostsCertificateRequest) (response *tcteo.ModifyHostsCertificateResponse, err error) {\n\treturn c.ModifyHostsCertificateWithContext(context.Background(), request)\n}\n\nfunc (c *TeoClient) ModifyHostsCertificateWithContext(ctx context.Context, request *tcteo.ModifyHostsCertificateRequest) (response *tcteo.ModifyHostsCertificateResponse, err error) {\n\tif request == nil {\n\t\trequest = tcteo.NewModifyHostsCertificateRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"teo\", tcteo.APIVersion, \"ModifyHostsCertificate\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"ModifyHostsCertificate require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcteo.NewModifyHostsCertificateResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo.go",
    "content": "package tencentcloudeo\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcteo \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-eo/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n\txcertkey \"github.com/certimate-go/certimate/pkg/utils/cert/key\"\n)\n\ntype DeployerConfig struct {\n\t// 腾讯云 SecretId。\n\tSecretId string `json:\"secretId\"`\n\t// 腾讯云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 腾讯云接口端点。\n\tEndpoint string `json:\"endpoint,omitempty\"`\n\t// 站点 ID。\n\tZoneId string `json:\"zoneId\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名列表（支持泛域名）。\n\tDomains []string `json:\"domains\"`\n\t// 是否启用多证书模式。\n\tEnableMultipleSSL bool `json:\"enableMultipleSSL,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.TeoClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tSecretId:  config.SecretId,\n\t\tSecretKey: config.SecretKey,\n\t\tEndpoint: lo.\n\t\t\tIf(strings.HasSuffix(config.Endpoint, \"intl.tencentcloudapi.com\"), \"ssl.intl.tencentcloudapi.com\"). // 国际站使用独立的接口端点\n\t\t\tElse(\"\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.ZoneId == \"\" {\n\t\treturn nil, errors.New(\"config `zoneId` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取全部可部署的域名信息\n\tdomainsInZone, err := d.getAllDomainsInZone(ctx, d.config.ZoneId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif len(d.config.Domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"config `domains` is required\")\n\t\t\t}\n\n\t\t\tdomains = d.config.Domains\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif len(d.config.Domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"config `domains` is required\")\n\t\t\t}\n\n\t\t\tdomainCandidates := lo.Map(domainsInZone, func(domainInfo *tcteo.AccelerationDomain, _ int) string {\n\t\t\t\treturn lo.FromPtr(domainInfo.DomainName)\n\t\t\t})\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\tfor _, configDomain := range d.config.Domains {\n\t\t\t\t\tif xcerthostname.IsMatch(configDomain, domain) {\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates := lo.Map(domainsInZone, func(domainInfo *tcteo.AccelerationDomain, _ int) string {\n\t\t\t\treturn lo.FromPtr(domainInfo.DomainName)\n\t\t\t})\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 跳过已部署过的域名\n\tdomains = lo.Filter(domains, func(domain string, _ int) bool {\n\t\tvar deployed bool\n\n\t\tdomainInfo, _ := lo.Find(domainsInZone, func(domainInfo *tcteo.AccelerationDomain) bool {\n\t\t\treturn domain == lo.FromPtr(domainInfo.DomainName)\n\t\t})\n\t\tif domainInfo != nil && domainInfo.Certificate != nil {\n\t\t\tdeployed = lo.ContainsBy(domainInfo.Certificate.List, func(certInfo *tcteo.CertificateInfo) bool {\n\t\t\t\treturn upres.CertId == lo.FromPtr(certInfo.CertId)\n\t\t\t})\n\t\t}\n\n\t\treturn !deployed\n\t})\n\n\t// 批量更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no edgeone domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found edgeone domains to deploy\", slog.Any(\"domains\", domains))\n\n\t\t// 配置域名证书\n\t\t// REF: https://cloud.tencent.com/document/api/1552/80764\n\t\tmodifyHostsCertificateReqs := make([]*tcteo.ModifyHostsCertificateRequest, 0)\n\n\t\tif d.config.EnableMultipleSSL {\n\t\t\tconst algRSA = \"RSA\"\n\t\t\tconst algECC = \"ECC\"\n\n\t\t\tprivkey, err := xcert.ParsePrivateKeyFromPEM(privkeyPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to parse private key: %w\", err)\n\t\t\t}\n\n\t\t\tprivkeyAlg, _, _ := xcertkey.GetPrivateKeyAlgorithm(privkey)\n\t\t\tprivkeyAlgStr := \"\"\n\t\t\tswitch privkeyAlg {\n\t\t\tcase x509.RSA:\n\t\t\t\tprivkeyAlgStr = algRSA\n\t\t\tcase x509.ECDSA:\n\t\t\t\tprivkeyAlgStr = algECC\n\t\t\t}\n\n\t\t\tfor _, domain := range domains {\n\t\t\t\tmodifyHostsCertificateReq := tcteo.NewModifyHostsCertificateRequest()\n\t\t\t\tmodifyHostsCertificateReq.ZoneId = common.StringPtr(d.config.ZoneId)\n\t\t\t\tmodifyHostsCertificateReq.Mode = common.StringPtr(\"sslcert\")\n\t\t\t\tmodifyHostsCertificateReq.Hosts = common.StringPtrs([]string{domain})\n\t\t\t\tmodifyHostsCertificateReq.ServerCertInfo = []*tcteo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}}\n\n\t\t\t\tdomainInfo, _ := lo.Find(domainsInZone, func(domainInfo *tcteo.AccelerationDomain) bool {\n\t\t\t\t\treturn domain == lo.FromPtr(domainInfo.DomainName)\n\t\t\t\t})\n\t\t\t\tif domainInfo != nil && domainInfo.Certificate != nil {\n\t\t\t\t\tfor _, certInfo := range domainInfo.Certificate.List {\n\t\t\t\t\t\tif lo.FromPtr(certInfo.CertId) == upres.CertId {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif strings.Split(lo.FromPtr(certInfo.SignAlgo), \" \")[0] == privkeyAlgStr {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcertExpireTime, _ := time.Parse(\"2006-01-02T15:04:05Z\", lo.FromPtr(certInfo.ExpireTime))\n\t\t\t\t\t\tif certExpireTime.Before(time.Now()) {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tmodifyHostsCertificateReq.ServerCertInfo = append(modifyHostsCertificateReq.ServerCertInfo, &tcteo.ServerCertInfo{CertId: certInfo.CertId})\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tmodifyHostsCertificateReqs = append(modifyHostsCertificateReqs, modifyHostsCertificateReq)\n\t\t\t}\n\t\t} else {\n\t\t\tmodifyHostsCertificateReq := tcteo.NewModifyHostsCertificateRequest()\n\t\t\tmodifyHostsCertificateReq.ZoneId = common.StringPtr(d.config.ZoneId)\n\t\t\tmodifyHostsCertificateReq.Mode = common.StringPtr(\"sslcert\")\n\t\t\tmodifyHostsCertificateReq.Hosts = common.StringPtrs(domains)\n\t\t\tmodifyHostsCertificateReq.ServerCertInfo = []*tcteo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}}\n\n\t\t\tmodifyHostsCertificateReqs = append(modifyHostsCertificateReqs, modifyHostsCertificateReq)\n\t\t}\n\n\t\tvar errs []error\n\t\tfor _, modifyHostsCertificateReq := range modifyHostsCertificateReqs {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tmodifyHostsCertificateResp, err := d.sdkClient.ModifyHostsCertificate(modifyHostsCertificateReq)\n\t\t\t\td.logger.Debug(\"sdk request 'teo.ModifyHostsCertificate'\", slog.Any(\"request\", modifyHostsCertificateReq), slog.Any(\"response\", modifyHostsCertificateResp))\n\t\t\t\tif err != nil {\n\t\t\t\t\terr = fmt.Errorf(\"failed to execute sdk request 'teo.ModifyHostsCertificate': %w\", err)\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomainsInZone(ctx context.Context, zoneId string) ([]*tcteo.AccelerationDomain, error) {\n\tvar domainsInZone []*tcteo.AccelerationDomain\n\n\tconst pageSize = 200\n\tfor offset := 0; ; offset += pageSize {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\t// 查询加速域名列表\n\t\t// REF: https://cloud.tencent.com/document/api/1552/86336\n\t\tdescribeAccelerationDomainsReq := tcteo.NewDescribeAccelerationDomainsRequest()\n\t\tdescribeAccelerationDomainsReq.Limit = common.Int64Ptr(pageSize)\n\t\tdescribeAccelerationDomainsReq.Offset = common.Int64Ptr(int64(offset))\n\t\tdescribeAccelerationDomainsReq.ZoneId = common.StringPtr(zoneId)\n\t\tdescribeAccelerationDomainsResp, err := d.sdkClient.DescribeAccelerationDomains(describeAccelerationDomainsReq)\n\t\td.logger.Debug(\"sdk request 'teo.DescribeAccelerationDomains'\", slog.Any(\"request\", describeAccelerationDomainsReq), slog.Any(\"response\", describeAccelerationDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'teo.DescribeAccelerationDomains': %w\", err)\n\t\t}\n\n\t\tignoredStatuses := []string{\"offline\", \"forbidden\", \"init\"}\n\t\tfor _, domainItem := range describeAccelerationDomainsResp.Response.AccelerationDomains {\n\t\t\tif lo.Contains(ignoredStatuses, lo.FromPtr(domainItem.DomainStatus)) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdomainsInZone = append(domainsInZone, domainItem)\n\t\t}\n\n\t\tif len(describeAccelerationDomainsResp.Response.AccelerationDomains) < pageSize {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn domainsInZone, nil\n}\n\nfunc createSDKClient(secretId, secretKey, endpoint string) (*internal.TeoClient, error) {\n\tcredential := common.NewCredential(secretId, secretKey)\n\n\tcpf := profile.NewClientProfile()\n\tif endpoint != \"\" {\n\t\tcpf.HttpProfile.Endpoint = endpoint\n\t}\n\n\tclient, err := internal.NewTeoClient(credential, \"\", cpf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go",
    "content": "package tencentcloudeo_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-eo\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfSecretId      string\n\tfSecretKey     string\n\tfZoneId        string\n\tfDomains       string\n)\n\nfunc init() {\n\targsPrefix := \"TENCENTCLOUDEO_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fSecretId, argsPrefix+\"SECRETID\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n\tflag.StringVar(&fZoneId, argsPrefix+\"ZONEID\", \"\", \"\")\n\tflag.StringVar(&fDomains, argsPrefix+\"DOMAINS\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./tencentcloud_eo_test.go -args \\\n\t--TENCENTCLOUDEO_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--TENCENTCLOUDEO_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--TENCENTCLOUDEO_SECRETID=\"your-secret-id\" \\\n\t--TENCENTCLOUDEO_SECRETKEY=\"your-secret-key\" \\\n\t--TENCENTCLOUDEO_ZONEID=\"your-zone-id\" \\\n\t--TENCENTCLOUDEO_DOMAINS=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SECRETID: %v\", fSecretId),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"ZONEID: %v\", fZoneId),\n\t\t\tfmt.Sprintf(\"DOMAINS: %v\", fDomains),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tSecretId:           fSecretId,\n\t\t\tSecretKey:          fSecretKey,\n\t\t\tZoneId:             fZoneId,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomains:            strings.Split(fDomains, \";\"),\n\t\t\tEnableMultipleSSL:  true,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-gaap/consts.go",
    "content": "package tencentcloudgaap\n\nconst (\n\t// 资源类型：部署到指定监听器。\n\tRESOURCE_TYPE_LISTENER = \"listener\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-gaap/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcgaap \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap/v20180529\"\n)\n\n// This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/gaap/v20180529/client.go\n// to lightweight the vendor packages in the built binary.\ntype GaapClient struct {\n\tcommon.Client\n}\n\nfunc NewGaapClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *GaapClient, err error) {\n\tclient = &GaapClient{}\n\tclient.Init(region).\n\t\tWithCredential(credential).\n\t\tWithProfile(clientProfile)\n\treturn\n}\n\nfunc (c *GaapClient) DescribeHTTPSListeners(request *tcgaap.DescribeHTTPSListenersRequest) (response *tcgaap.DescribeHTTPSListenersResponse, err error) {\n\treturn c.DescribeHTTPSListenersWithContext(context.Background(), request)\n}\n\nfunc (c *GaapClient) DescribeHTTPSListenersWithContext(ctx context.Context, request *tcgaap.DescribeHTTPSListenersRequest) (response *tcgaap.DescribeHTTPSListenersResponse, err error) {\n\tif request == nil {\n\t\trequest = tcgaap.NewDescribeHTTPSListenersRequest()\n\t}\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeHTTPSListeners require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcgaap.NewDescribeHTTPSListenersResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *GaapClient) ModifyHTTPSListenerAttribute(request *tcgaap.ModifyHTTPSListenerAttributeRequest) (response *tcgaap.ModifyHTTPSListenerAttributeResponse, err error) {\n\treturn c.ModifyHTTPSListenerAttributeWithContext(context.Background(), request)\n}\n\nfunc (c *GaapClient) ModifyHTTPSListenerAttributeWithContext(ctx context.Context, request *tcgaap.ModifyHTTPSListenerAttributeRequest) (response *tcgaap.ModifyHTTPSListenerAttributeResponse, err error) {\n\tif request == nil {\n\t\trequest = tcgaap.NewModifyHTTPSListenerAttributeRequest()\n\t}\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"ModifyHTTPSListenerAttribute require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcgaap.NewModifyHTTPSListenerAttributeResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-gaap/tencentcloud_gaap.go",
    "content": "package tencentcloudgaap\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcgaap \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap/v20180529\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-gaap/internal\"\n)\n\ntype DeployerConfig struct {\n\t// 腾讯云 SecretId。\n\tSecretId string `json:\"secretId\"`\n\t// 腾讯云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 腾讯云接口端点。\n\tEndpoint string `json:\"endpoint,omitempty\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 通道 ID。\n\t// 选填。\n\tProxyId string `json:\"proxyId,omitempty\"`\n\t// 负载均衡监听 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。\n\tListenerId string `json:\"listenerId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.GaapClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClients(config.SecretId, config.SecretKey, config.Endpoint)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tSecretId:  config.SecretId,\n\t\tSecretKey: config.SecretKey,\n\t\tEndpoint: lo.\n\t\t\tIf(strings.HasSuffix(config.Endpoint, \"intl.tencentcloudapi.com\"), \"ssl.intl.tencentcloudapi.com\"). // 国际站使用独立的接口端点\n\t\t\tElse(\"\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_LISTENER:\n\t\tif err := d.deployToListener(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error {\n\tif d.config.ListenerId == \"\" {\n\t\treturn errors.New(\"config `listenerId` is required\")\n\t}\n\n\t// 更新监听器证书\n\tif err := d.updateHttpsListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateHttpsListenerCertificate(ctx context.Context, cloudListenerId, cloudCertId string) error {\n\t// 查询 HTTPS 监听器信息\n\t// REF: https://cloud.tencent.com/document/api/608/37001\n\tdescribeHTTPSListenersReq := tcgaap.NewDescribeHTTPSListenersRequest()\n\tdescribeHTTPSListenersReq.ListenerId = common.StringPtr(cloudListenerId)\n\tdescribeHTTPSListenersReq.Offset = common.Uint64Ptr(0)\n\tdescribeHTTPSListenersReq.Limit = common.Uint64Ptr(1)\n\tdescribeHTTPSListenersResp, err := d.sdkClient.DescribeHTTPSListeners(describeHTTPSListenersReq)\n\td.logger.Debug(\"sdk request 'gaap.DescribeHTTPSListeners'\", slog.Any(\"request\", describeHTTPSListenersReq), slog.Any(\"response\", describeHTTPSListenersResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'gaap.DescribeHTTPSListeners': %w\", err)\n\t} else if len(describeHTTPSListenersResp.Response.ListenerSet) == 0 {\n\t\treturn fmt.Errorf(\"could not find listener '%s'\", cloudListenerId)\n\t}\n\n\t// 修改 HTTPS 监听器配置\n\t// REF: https://cloud.tencent.com/document/api/608/36996\n\tmodifyHTTPSListenerAttributeReq := tcgaap.NewModifyHTTPSListenerAttributeRequest()\n\tmodifyHTTPSListenerAttributeReq.ProxyId = lo.EmptyableToPtr(d.config.ProxyId)\n\tmodifyHTTPSListenerAttributeReq.ListenerId = common.StringPtr(cloudListenerId)\n\tmodifyHTTPSListenerAttributeReq.CertificateId = common.StringPtr(cloudCertId)\n\tmodifyHTTPSListenerAttributeResp, err := d.sdkClient.ModifyHTTPSListenerAttribute(modifyHTTPSListenerAttributeReq)\n\td.logger.Debug(\"sdk request 'gaap.ModifyHTTPSListenerAttribute'\", slog.Any(\"request\", modifyHTTPSListenerAttributeReq), slog.Any(\"response\", modifyHTTPSListenerAttributeResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'gaap.ModifyHTTPSListenerAttribute': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClients(secretId, secretKey, endpoint string) (*internal.GaapClient, error) {\n\tcredential := common.NewCredential(secretId, secretKey)\n\n\tcpf := profile.NewClientProfile()\n\tif endpoint != \"\" {\n\t\tcpf.HttpProfile.Endpoint = endpoint\n\t}\n\n\tclient, err := internal.NewGaapClient(credential, \"\", cpf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-gaap/tencentcloud_gaap_test.go",
    "content": "package tencentcloudgaap_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-gaap\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfSecretId      string\n\tfSecretKey     string\n\tfProxyId       string\n\tfListenerId    string\n)\n\nfunc init() {\n\targsPrefix := \"TENCENTCLOUDCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fSecretId, argsPrefix+\"SECRETID\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n\tflag.StringVar(&fProxyId, argsPrefix+\"PROXYID\", \"\", \"\")\n\tflag.StringVar(&fListenerId, argsPrefix+\"LISTENERID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./tencentcloud_gaap_test.go -args \\\n\t--TENCENTCLOUDGAAP_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--TENCENTCLOUDGAAP_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--TENCENTCLOUDGAAP_SECRETID=\"your-secret-id\" \\\n\t--TENCENTCLOUDGAAP_SECRETKEY=\"your-secret-key\" \\\n\t--TENCENTCLOUDGAAP_PROXYID=\"your-gaap-group-id\" \\\n\t--TENCENTCLOUDGAAP_LISTENERID=\"your-clb-listener-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy_ToListener\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SECRETID: %v\", fSecretId),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"PROXYID: %v\", fProxyId),\n\t\t\tfmt.Sprintf(\"LISTENERID: %v\", fListenerId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tSecretId:     fSecretId,\n\t\t\tSecretKey:    fSecretKey,\n\t\t\tResourceType: provider.RESOURCE_TYPE_LISTENER,\n\t\t\tProxyId:      fProxyId,\n\t\t\tListenerId:   fListenerId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-scf/consts.go",
    "content": "package tencentcloudscf\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-scf/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcscf \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf/v20180416\"\n)\n\n// This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/scf/v20180416/client.go\n// to lightweight the vendor packages in the built binary.\ntype ScfClient struct {\n\tcommon.Client\n}\n\nfunc NewScfClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *ScfClient, err error) {\n\tclient = &ScfClient{}\n\tclient.Init(region).\n\t\tWithCredential(credential).\n\t\tWithProfile(clientProfile)\n\treturn\n}\n\nfunc (c *ScfClient) GetCustomDomain(request *tcscf.GetCustomDomainRequest) (response *tcscf.GetCustomDomainResponse, err error) {\n\treturn c.GetCustomDomainWithContext(context.Background(), request)\n}\n\nfunc (c *ScfClient) GetCustomDomainWithContext(ctx context.Context, request *tcscf.GetCustomDomainRequest) (response *tcscf.GetCustomDomainResponse, err error) {\n\tif request == nil {\n\t\trequest = tcscf.NewGetCustomDomainRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"scf\", tcscf.APIVersion, \"GetCustomDomain\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"GetCustomDomain require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcscf.NewGetCustomDomainResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *ScfClient) ListCustomDomains(request *tcscf.ListCustomDomainsRequest) (response *tcscf.ListCustomDomainsResponse, err error) {\n\treturn c.ListCustomDomainsWithContext(context.Background(), request)\n}\n\nfunc (c *ScfClient) ListCustomDomainsWithContext(ctx context.Context, request *tcscf.ListCustomDomainsRequest) (response *tcscf.ListCustomDomainsResponse, err error) {\n\tif request == nil {\n\t\trequest = tcscf.NewListCustomDomainsRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"scf\", tcscf.APIVersion, \"ListCustomDomains\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"ListCustomDomains require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\n\tresponse = tcscf.NewListCustomDomainsResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *ScfClient) UpdateCustomDomain(request *tcscf.UpdateCustomDomainRequest) (response *tcscf.UpdateCustomDomainResponse, err error) {\n\treturn c.UpdateCustomDomainWithContext(context.Background(), request)\n}\n\nfunc (c *ScfClient) UpdateCustomDomainWithContext(ctx context.Context, request *tcscf.UpdateCustomDomainRequest) (response *tcscf.UpdateCustomDomainResponse, err error) {\n\tif request == nil {\n\t\trequest = tcscf.NewUpdateCustomDomainRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"scf\", tcscf.APIVersion, \"UpdateCustomDomain\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"UpdateCustomDomain require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcscf.NewUpdateCustomDomainResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-scf/tencentcloud_scf.go",
    "content": "package tencentcloudscf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcscf \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf/v20180416\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-scf/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n)\n\ntype DeployerConfig struct {\n\t// 腾讯云 SecretId。\n\tSecretId string `json:\"secretId\"`\n\t// 腾讯云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 腾讯云接口端点。\n\tEndpoint string `json:\"endpoint,omitempty\"`\n\t// 腾讯云地域。\n\tRegion string `json:\"region\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 自定义域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.ScfClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tSecretId:  config.SecretId,\n\t\tSecretKey: config.SecretKey,\n\t\tEndpoint: lo.\n\t\t\tIf(strings.HasSuffix(config.Endpoint, \"intl.tencentcloudapi.com\"), \"ssl.intl.tencentcloudapi.com\"). // 国际站使用独立的接口端点\n\t\t\tElse(\"\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no scf domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found scf domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 获取云函数自定义域名列表\n\t// REF: https://cloud.tencent.com/document/api/583/111923\n\tlistCustomDomainsOffset := 0\n\tlistCustomDomainsLimit := 20\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeLiveDomainsReq := tcscf.NewListCustomDomainsRequest()\n\t\tdescribeLiveDomainsReq.Offset = common.Uint64Ptr(uint64(listCustomDomainsOffset))\n\t\tdescribeLiveDomainsReq.Limit = common.Uint64Ptr(uint64(listCustomDomainsLimit))\n\t\tdescribeLiveDomainsResp, err := d.sdkClient.ListCustomDomains(describeLiveDomainsReq)\n\t\td.logger.Debug(\"sdk request 'scf.DescribeLiveDomains'\", slog.Any(\"request\", describeLiveDomainsReq), slog.Any(\"response\", describeLiveDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'scf.DescribeLiveDomains': %w\", err)\n\t\t}\n\n\t\tif describeLiveDomainsResp.Response == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, domainItem := range describeLiveDomainsResp.Response.Domains {\n\t\t\tdomains = append(domains, *domainItem.Domain)\n\t\t}\n\n\t\tif len(describeLiveDomainsResp.Response.Domains) < listCustomDomainsLimit {\n\t\t\tbreak\n\t\t}\n\n\t\tlistCustomDomainsOffset++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error {\n\t// 查看云函数自定义域名详情\n\t// REF: https://cloud.tencent.com/document/api/583/111924\n\tgetCustomDomainReq := tcscf.NewGetCustomDomainRequest()\n\tgetCustomDomainReq.Domain = common.StringPtr(domain)\n\tgetCustomDomainResp, err := d.sdkClient.GetCustomDomain(getCustomDomainReq)\n\td.logger.Debug(\"sdk request 'scf.GetCustomDomain'\", slog.Any(\"request\", getCustomDomainReq), slog.Any(\"response\", getCustomDomainResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'scf.GetCustomDomain': %w\", err)\n\t} else {\n\t\tif getCustomDomainResp.Response.CertConfig != nil && getCustomDomainResp.Response.CertConfig.CertificateId != nil && *getCustomDomainResp.Response.CertConfig.CertificateId == cloudCertId {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// 更新云函数自定义域名\n\t// REF: https://cloud.tencent.com/document/api/583/111922\n\tupdateCustomDomainReq := tcscf.NewUpdateCustomDomainRequest()\n\tupdateCustomDomainReq.Domain = common.StringPtr(domain)\n\tupdateCustomDomainReq.CertConfig = &tcscf.CertConf{\n\t\tCertificateId: common.StringPtr(cloudCertId),\n\t}\n\tupdateCustomDomainReq.Protocol = getCustomDomainResp.Response.Protocol\n\tif updateCustomDomainReq.Protocol == nil || *updateCustomDomainReq.Protocol == \"HTTP\" {\n\t\tupdateCustomDomainReq.Protocol = common.StringPtr(\"HTTP&HTTPS\")\n\t}\n\tupdateCustomDomainResp, err := d.sdkClient.UpdateCustomDomain(updateCustomDomainReq)\n\td.logger.Debug(\"sdk request 'scf.UpdateCustomDomain'\", slog.Any(\"request\", updateCustomDomainReq), slog.Any(\"response\", updateCustomDomainResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'scf.UpdateCustomDomain': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(secretId, secretKey, endpoint, region string) (*internal.ScfClient, error) {\n\tcredential := common.NewCredential(secretId, secretKey)\n\n\tcpf := profile.NewClientProfile()\n\tif endpoint != \"\" {\n\t\tcpf.HttpProfile.Endpoint = endpoint\n\t}\n\n\tclient, err := internal.NewScfClient(credential, region, cpf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-scf/tencentcloud_scf_test.go",
    "content": "package tencentcloudscf_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-scf\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfSecretId      string\n\tfSecretKey     string\n\tfRegion        string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"TENCENTCLOUDSCF_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fSecretId, argsPrefix+\"SECRETID\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./tencentcloud_scf_test.go -args \\\n\t--TENCENTCLOUDSCF_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--TENCENTCLOUDSCF_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--TENCENTCLOUDSCF_SECRETID=\"your-secret-id\" \\\n\t--TENCENTCLOUDSCF_SECRETKEY=\"your-secret-key\" \\\n\t--TENCENTCLOUDSCF_REGION=\"ap-guangzhou\" \\\n\t--TENCENTCLOUDSCF_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SECRETID: %v\", fSecretId),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tSecretId:           fSecretId,\n\t\t\tSecretKey:          fSecretKey,\n\t\t\tRegion:             fRegion,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-ssl/tencentcloud_ssl.go",
    "content": "package tencentcloudssl\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// 腾讯云 SecretId。\n\tSecretId string `json:\"secretId\"`\n\t// 腾讯云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 腾讯云接口端点。\n\tEndpoint string `json:\"endpoint,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tSecretId:  config.SecretId,\n\t\tSecretKey: config.SecretKey,\n\t\tEndpoint:  config.Endpoint,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-ssl-deploy/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcssl \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205\"\n)\n\n// This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/ssl/v20191205/client.go\n// to lightweight the vendor packages in the built binary.\ntype SslClient struct {\n\tcommon.Client\n}\n\nfunc NewSslClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *SslClient, err error) {\n\tclient = &SslClient{}\n\tclient.Init(region).\n\t\tWithCredential(credential).\n\t\tWithProfile(clientProfile)\n\treturn\n}\n\nfunc (c *SslClient) DeployCertificateInstance(request *tcssl.DeployCertificateInstanceRequest) (response *tcssl.DeployCertificateInstanceResponse, err error) {\n\treturn c.DeployCertificateInstanceWithContext(context.Background(), request)\n}\n\nfunc (c *SslClient) DeployCertificateInstanceWithContext(ctx context.Context, request *tcssl.DeployCertificateInstanceRequest) (response *tcssl.DeployCertificateInstanceResponse, err error) {\n\tif request == nil {\n\t\trequest = tcssl.NewDeployCertificateInstanceRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"ssl\", tcssl.APIVersion, \"DeployCertificateInstance\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DeployCertificateInstance require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcssl.NewDeployCertificateInstanceResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *SslClient) DescribeHostDeployRecordDetail(request *tcssl.DescribeHostDeployRecordDetailRequest) (response *tcssl.DescribeHostDeployRecordDetailResponse, err error) {\n\treturn c.DescribeHostDeployRecordDetailWithContext(context.Background(), request)\n}\n\nfunc (c *SslClient) DescribeHostDeployRecordDetailWithContext(ctx context.Context, request *tcssl.DescribeHostDeployRecordDetailRequest) (response *tcssl.DescribeHostDeployRecordDetailResponse, err error) {\n\tif request == nil {\n\t\trequest = tcssl.NewDescribeHostDeployRecordDetailRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"ssl\", tcssl.APIVersion, \"DescribeHostDeployRecordDetail\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeHostDeployRecordDetail require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcssl.NewDescribeHostDeployRecordDetailResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-ssl-deploy/tencentcloud_ssl_deploy.go",
    "content": "package tencentcloudssldeploy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcssl \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ssl-deploy/internal\"\n\txwait \"github.com/certimate-go/certimate/pkg/utils/wait\"\n)\n\ntype DeployerConfig struct {\n\t// 腾讯云 SecretId。\n\tSecretId string `json:\"secretId\"`\n\t// 腾讯云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 腾讯云接口端点。\n\tEndpoint string `json:\"endpoint,omitempty\"`\n\t// 腾讯云地域。\n\tRegion string `json:\"region\"`\n\t// 云产品类型。\n\tResourceProduct string `json:\"resourceProduct\"`\n\t// 云产品资源 ID 数组。\n\tResourceIds []string `json:\"resourceIds,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.SslClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tSecretId:  config.SecretId,\n\t\tSecretKey: config.SecretKey,\n\t\tEndpoint:  config.Endpoint,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.ResourceProduct == \"\" {\n\t\treturn nil, errors.New(\"config `resourceProduct` is required\")\n\t}\n\tif len(d.config.ResourceIds) == 0 {\n\t\treturn nil, errors.New(\"config `resourceIds` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 证书部署到云资源实例列表\n\t// REF: https://cloud.tencent.com/document/api/400/91667\n\tdeployCertificateInstanceReq := tcssl.NewDeployCertificateInstanceRequest()\n\tdeployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)\n\tdeployCertificateInstanceReq.ResourceType = common.StringPtr(d.config.ResourceProduct)\n\tdeployCertificateInstanceReq.InstanceIdList = common.StringPtrs(d.config.ResourceIds)\n\tdeployCertificateInstanceReq.Status = common.Int64Ptr(1)\n\tdeployCertificateInstanceResp, err := d.sdkClient.DeployCertificateInstance(deployCertificateInstanceReq)\n\td.logger.Debug(\"sdk request 'ssl.DeployCertificateInstance'\", slog.Any(\"request\", deployCertificateInstanceReq), slog.Any(\"response\", deployCertificateInstanceResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'ssl.DeployCertificateInstance': %w\", err)\n\t} else if deployCertificateInstanceResp.Response == nil || deployCertificateInstanceResp.Response.DeployRecordId == nil {\n\t\treturn nil, errors.New(\"failed to create deploy record\")\n\t}\n\n\t// 获取部署任务详情，等待任务状态变更\n\t// REF: https://cloud.tencent.com/document/api/400/91658\n\tif _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) {\n\t\tdescribeHostDeployRecordDetailReq := tcssl.NewDescribeHostDeployRecordDetailRequest()\n\t\tdescribeHostDeployRecordDetailReq.DeployRecordId = common.StringPtr(fmt.Sprintf(\"%d\", *deployCertificateInstanceResp.Response.DeployRecordId))\n\t\tdescribeHostDeployRecordDetailReq.Limit = common.Uint64Ptr(200)\n\t\tdescribeHostDeployRecordDetailResp, err := d.sdkClient.DescribeHostDeployRecordDetail(describeHostDeployRecordDetailReq)\n\t\td.logger.Debug(\"sdk request 'ssl.DescribeHostDeployRecordDetail'\", slog.Any(\"request\", describeHostDeployRecordDetailReq), slog.Any(\"response\", describeHostDeployRecordDetailResp))\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to execute sdk request 'ssl.DescribeHostDeployRecordDetail': %w\", err)\n\t\t}\n\n\t\tvar pendingCount, runningCount, succeededCount, failedCount, totalCount int64\n\t\tif describeHostDeployRecordDetailResp.Response.TotalCount == nil {\n\t\t\treturn false, fmt.Errorf(\"unexpected tencentcloud deployment job status\")\n\t\t} else {\n\t\t\tpendingCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.PendingTotalCount)\n\t\t\trunningCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.RunningTotalCount)\n\t\t\tsucceededCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.SuccessTotalCount)\n\t\t\tfailedCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.FailedTotalCount)\n\t\t\ttotalCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.TotalCount)\n\n\t\t\tif succeededCount+failedCount == totalCount {\n\t\t\t\tif failedCount > 0 {\n\t\t\t\t\treturn false, fmt.Errorf(\"tencentcloud deployment job failed (succeeded: %d, failed: %d, total: %d)\", succeededCount, failedCount, totalCount)\n\t\t\t\t}\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t}\n\n\t\td.logger.Info(fmt.Sprintf(\"waiting for tencentcloud deployment job completion (pending: %d, running: %d, succeeded: %d, failed: %d, total: %d) ...\", pendingCount, runningCount, succeededCount, failedCount, totalCount))\n\t\treturn false, nil\n\t}, time.Second*5); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(secretId, secretKey, endpoint, region string) (*internal.SslClient, error) {\n\tcredential := common.NewCredential(secretId, secretKey)\n\n\tcpf := profile.NewClientProfile()\n\tif endpoint != \"\" {\n\t\tcpf.HttpProfile.Endpoint = endpoint\n\t}\n\n\tclient, err := internal.NewSslClient(credential, region, cpf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-ssl-update/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcssl \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205\"\n)\n\n// This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/ssl/v20191205/client.go\n// to lightweight the vendor packages in the built binary.\ntype SslClient struct {\n\tcommon.Client\n}\n\nfunc NewSslClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *SslClient, err error) {\n\tclient = &SslClient{}\n\tclient.Init(region).\n\t\tWithCredential(credential).\n\t\tWithProfile(clientProfile)\n\treturn\n}\n\nfunc (c *SslClient) DescribeHostUpdateRecordDetail(request *tcssl.DescribeHostUpdateRecordDetailRequest) (response *tcssl.DescribeHostUpdateRecordDetailResponse, err error) {\n\treturn c.DescribeHostUpdateRecordDetailWithContext(context.Background(), request)\n}\n\nfunc (c *SslClient) DescribeHostUpdateRecordDetailWithContext(ctx context.Context, request *tcssl.DescribeHostUpdateRecordDetailRequest) (response *tcssl.DescribeHostUpdateRecordDetailResponse, err error) {\n\tif request == nil {\n\t\trequest = tcssl.NewDescribeHostUpdateRecordDetailRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"ssl\", tcssl.APIVersion, \"DescribeHostUpdateRecordDetail\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeHostUpdateRecordDetail require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcssl.NewDescribeHostUpdateRecordDetailResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *SslClient) DescribeHostUploadUpdateRecordDetail(request *tcssl.DescribeHostUploadUpdateRecordDetailRequest) (response *tcssl.DescribeHostUploadUpdateRecordDetailResponse, err error) {\n\treturn c.DescribeHostUploadUpdateRecordDetailWithContext(context.Background(), request)\n}\n\nfunc (c *SslClient) DescribeHostUploadUpdateRecordDetailWithContext(ctx context.Context, request *tcssl.DescribeHostUploadUpdateRecordDetailRequest) (response *tcssl.DescribeHostUploadUpdateRecordDetailResponse, err error) {\n\tif request == nil {\n\t\trequest = tcssl.NewDescribeHostUploadUpdateRecordDetailRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"ssl\", tcssl.APIVersion, \"DescribeHostUploadUpdateRecordDetail\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeHostUploadUpdateRecordDetail require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcssl.NewDescribeHostUploadUpdateRecordDetailResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *SslClient) UpdateCertificateInstance(request *tcssl.UpdateCertificateInstanceRequest) (response *tcssl.UpdateCertificateInstanceResponse, err error) {\n\treturn c.UpdateCertificateInstanceWithContext(context.Background(), request)\n}\n\nfunc (c *SslClient) UpdateCertificateInstanceWithContext(ctx context.Context, request *tcssl.UpdateCertificateInstanceRequest) (response *tcssl.UpdateCertificateInstanceResponse, err error) {\n\tif request == nil {\n\t\trequest = tcssl.NewUpdateCertificateInstanceRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"ssl\", tcssl.APIVersion, \"UpdateCertificateInstance\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"UpdateCertificateInstance require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\n\tresponse = tcssl.NewUpdateCertificateInstanceResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *SslClient) UploadUpdateCertificateInstance(request *tcssl.UploadUpdateCertificateInstanceRequest) (response *tcssl.UploadUpdateCertificateInstanceResponse, err error) {\n\treturn c.UploadUpdateCertificateInstanceWithContext(context.Background(), request)\n}\n\nfunc (c *SslClient) UploadUpdateCertificateInstanceWithContext(ctx context.Context, request *tcssl.UploadUpdateCertificateInstanceRequest) (response *tcssl.UploadUpdateCertificateInstanceResponse, err error) {\n\tif request == nil {\n\t\trequest = tcssl.NewUploadUpdateCertificateInstanceRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"ssl\", tcssl.APIVersion, \"UploadUpdateCertificateInstance\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"UploadUpdateCertificateInstance require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcssl.NewUploadUpdateCertificateInstanceResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-ssl-update/tencentcloud_ssl_update.go",
    "content": "package tencentcloudsslupdate\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcssl \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ssl-update/internal\"\n\txwait \"github.com/certimate-go/certimate/pkg/utils/wait\"\n)\n\ntype DeployerConfig struct {\n\t// 腾讯云 SecretId。\n\tSecretId string `json:\"secretId\"`\n\t// 腾讯云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 腾讯云接口端点。\n\tEndpoint string `json:\"endpoint,omitempty\"`\n\t// 原证书 ID。\n\tCertificateId string `json:\"certificateId\"`\n\t// 是否替换原有证书（即保持原证书 ID 不变）。\n\tIsReplaced bool `json:\"isReplaced,omitempty\"`\n\t// 云产品类型数组。\n\tResourceProducts []string `json:\"resourceProducts\"`\n\t// 云产品地域数组。\n\tResourceRegions []string `json:\"resourceRegions\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.SslClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tSecretId:  config.SecretId,\n\t\tSecretKey: config.SecretKey,\n\t\tEndpoint:  config.Endpoint,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.CertificateId == \"\" {\n\t\treturn nil, errors.New(\"config `certificateId` is required\")\n\t}\n\tif len(d.config.ResourceProducts) == 0 {\n\t\treturn nil, errors.New(\"config `resourceProducts` is required\")\n\t}\n\n\tif d.config.IsReplaced {\n\t\tif err := d.executeUploadUpdateCertificateInstance(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tif err := d.executeUpdateCertificateInstance(ctx, certPEM, privkeyPEM); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) executeUpdateCertificateInstance(ctx context.Context, certPEM, privkeyPEM string) error {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 一键更新新旧证书资源\n\t// REF: https://cloud.tencent.com/document/product/400/91649\n\tvar deployRecordId string\n\tif _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) {\n\t\tupdateCertificateInstanceReq := tcssl.NewUpdateCertificateInstanceRequest()\n\t\tupdateCertificateInstanceReq.OldCertificateId = common.StringPtr(d.config.CertificateId)\n\t\tupdateCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)\n\t\tupdateCertificateInstanceReq.ResourceTypes = common.StringPtrs(d.config.ResourceProducts)\n\t\tupdateCertificateInstanceReq.ResourceTypesRegions = wrapResourceProductRegions(d.config.ResourceProducts, d.config.ResourceRegions)\n\t\tupdateCertificateInstanceResp, err := d.sdkClient.UpdateCertificateInstance(updateCertificateInstanceReq)\n\t\td.logger.Debug(\"sdk request 'ssl.UpdateCertificateInstance'\", slog.Any(\"request\", updateCertificateInstanceReq), slog.Any(\"response\", updateCertificateInstanceResp))\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to execute sdk request 'ssl.UpdateCertificateInstance': %w\", err)\n\t\t}\n\n\t\tif updateCertificateInstanceResp.Response.DeployStatus == nil || updateCertificateInstanceResp.Response.DeployRecordId == nil {\n\t\t\treturn false, fmt.Errorf(\"unexpected deployment job status\")\n\t\t} else if *updateCertificateInstanceResp.Response.DeployRecordId > 0 {\n\t\t\tdeployRecordId = fmt.Sprintf(\"%d\", *updateCertificateInstanceResp.Response.DeployRecordId)\n\t\t\treturn true, nil\n\t\t}\n\n\t\treturn false, nil\n\t}, time.Second*5); err != nil {\n\t\treturn err\n\t}\n\n\t// 查询证书云资源更新记录详情，等待任务状态变更\n\t// REF: https://cloud.tencent.com/document/api/400/91652\n\tif _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) {\n\t\tdescribeHostUpdateRecordDetailReq := tcssl.NewDescribeHostUpdateRecordDetailRequest()\n\t\tdescribeHostUpdateRecordDetailReq.DeployRecordId = common.StringPtr(deployRecordId)\n\t\tdescribeHostUpdateRecordDetailReq.Limit = common.StringPtr(\"200\")\n\t\tdescribeHostUpdateRecordDetailResp, err := d.sdkClient.DescribeHostUpdateRecordDetail(describeHostUpdateRecordDetailReq)\n\t\td.logger.Debug(\"sdk request 'ssl.DescribeHostUpdateRecordDetail'\", slog.Any(\"request\", describeHostUpdateRecordDetailReq), slog.Any(\"response\", describeHostUpdateRecordDetailResp))\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to execute sdk request 'ssl.DescribeHostUpdateRecordDetail': %w\", err)\n\t\t}\n\n\t\tvar pendingCount, runningCount, succeededCount, failedCount, totalCount int64\n\t\tif describeHostUpdateRecordDetailResp.Response.TotalCount == nil {\n\t\t\treturn false, fmt.Errorf(\"unexpected tencentcloud deployment job status\")\n\t\t} else {\n\t\t\tpendingCount = lo.FromPtr(describeHostUpdateRecordDetailResp.Response.PendingTotalCount)\n\t\t\trunningCount = lo.FromPtr(describeHostUpdateRecordDetailResp.Response.RunningTotalCount)\n\t\t\tsucceededCount = lo.FromPtr(describeHostUpdateRecordDetailResp.Response.SuccessTotalCount)\n\t\t\tfailedCount = lo.FromPtr(describeHostUpdateRecordDetailResp.Response.FailedTotalCount)\n\t\t\ttotalCount = lo.FromPtr(describeHostUpdateRecordDetailResp.Response.TotalCount)\n\n\t\t\tif succeededCount+failedCount == totalCount {\n\t\t\t\tif failedCount > 0 {\n\t\t\t\t\treturn false, fmt.Errorf(\"tencentcloud deployment job failed (succeeded: %d, failed: %d, total: %d)\", succeededCount, failedCount, totalCount)\n\t\t\t\t}\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t}\n\n\t\td.logger.Info(fmt.Sprintf(\"waiting for tencentcloud deployment job completion (pending: %d, running: %d, succeeded: %d, failed: %d, total: %d) ...\", pendingCount, runningCount, succeededCount, failedCount, totalCount))\n\t\treturn false, nil\n\t}, time.Second*5); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) executeUploadUpdateCertificateInstance(ctx context.Context, certPEM, privkeyPEM string) error {\n\t// 更新证书内容并更新关联的云资源\n\t// REF: https://cloud.tencent.com/document/product/400/119791\n\tvar deployRecordId int64\n\tif _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) {\n\t\tuploadUpdateCertificateInstanceReq := tcssl.NewUploadUpdateCertificateInstanceRequest()\n\t\tuploadUpdateCertificateInstanceReq.OldCertificateId = common.StringPtr(d.config.CertificateId)\n\t\tuploadUpdateCertificateInstanceReq.CertificatePublicKey = common.StringPtr(certPEM)\n\t\tuploadUpdateCertificateInstanceReq.CertificatePrivateKey = common.StringPtr(privkeyPEM)\n\t\tuploadUpdateCertificateInstanceReq.ResourceTypes = common.StringPtrs(d.config.ResourceProducts)\n\t\tuploadUpdateCertificateInstanceReq.ResourceTypesRegions = wrapResourceProductRegions(d.config.ResourceProducts, d.config.ResourceRegions)\n\t\tuploadUpdateCertificateInstanceResp, err := d.sdkClient.UploadUpdateCertificateInstance(uploadUpdateCertificateInstanceReq)\n\t\td.logger.Debug(\"sdk request 'ssl.UploadUpdateCertificateInstance'\", slog.Any(\"request\", uploadUpdateCertificateInstanceReq), slog.Any(\"response\", uploadUpdateCertificateInstanceResp))\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to execute sdk request 'ssl.UploadUpdateCertificateInstance': %w\", err)\n\t\t}\n\n\t\tif uploadUpdateCertificateInstanceResp.Response.DeployStatus == nil {\n\t\t\treturn false, fmt.Errorf(\"unexpected deployment job status\")\n\t\t} else if *uploadUpdateCertificateInstanceResp.Response.DeployStatus == 1 {\n\t\t\tdeployRecordId = int64(*uploadUpdateCertificateInstanceResp.Response.DeployRecordId)\n\t\t\treturn true, nil\n\t\t}\n\n\t\treturn false, nil\n\t}, time.Second*5); err != nil {\n\t\treturn err\n\t}\n\n\t// 查询证书云资源更新记录详情，等待任务状态变更\n\t// REF: https://cloud.tencent.com/document/product/400/120056\n\tif _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) {\n\t\tdescribeHostUploadUpdateRecordDetailReq := tcssl.NewDescribeHostUploadUpdateRecordDetailRequest()\n\t\tdescribeHostUploadUpdateRecordDetailReq.DeployRecordId = common.Int64Ptr(deployRecordId)\n\t\tdescribeHostUploadUpdateRecordDetailReq.Limit = common.Int64Ptr(200)\n\t\tdescribeHostUploadUpdateRecordDetailResp, err := d.sdkClient.DescribeHostUploadUpdateRecordDetail(describeHostUploadUpdateRecordDetailReq)\n\t\td.logger.Debug(\"sdk request 'ssl.DescribeHostUploadUpdateRecordDetail'\", slog.Any(\"request\", describeHostUploadUpdateRecordDetailReq), slog.Any(\"response\", describeHostUploadUpdateRecordDetailResp))\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to execute sdk request 'ssl.DescribeHostUploadUpdateRecordDetail': %w\", err)\n\t\t}\n\n\t\tvar runningCount, succeededCount, failedCount, totalCount int64\n\t\tif describeHostUploadUpdateRecordDetailResp.Response.DeployRecordDetail == nil {\n\t\t\treturn false, fmt.Errorf(\"unexpected tencentcloud deployment job status\")\n\t\t} else {\n\t\t\tfor _, record := range describeHostUploadUpdateRecordDetailResp.Response.DeployRecordDetail {\n\t\t\t\trunningCount += lo.FromPtr(record.RunningTotalCount)\n\t\t\t\tsucceededCount += lo.FromPtr(record.SuccessTotalCount)\n\t\t\t\tfailedCount += lo.FromPtr(record.FailedTotalCount)\n\t\t\t\ttotalCount += lo.FromPtr(record.TotalCount)\n\t\t\t}\n\n\t\t\tif succeededCount+failedCount == totalCount {\n\t\t\t\tif failedCount > 0 {\n\t\t\t\t\treturn false, fmt.Errorf(\"tencentcloud deployment job failed (succeeded: %d, failed: %d, total: %d)\", succeededCount, failedCount, totalCount)\n\t\t\t\t}\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t}\n\n\t\td.logger.Info(fmt.Sprintf(\"waiting for tencentcloud deployment job completion (running: %d, succeeded: %d, failed: %d, total: %d) ...\", runningCount, succeededCount, failedCount, totalCount))\n\t\treturn false, nil\n\t}, time.Second*5); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(secretId, secretKey, endpoint string) (*internal.SslClient, error) {\n\tcredential := common.NewCredential(secretId, secretKey)\n\n\tcpf := profile.NewClientProfile()\n\tif endpoint != \"\" {\n\t\tcpf.HttpProfile.Endpoint = endpoint\n\t}\n\n\tclient, err := internal.NewSslClient(credential, \"\", cpf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n\nfunc wrapResourceProductRegions(resourceProducts, resourceRegions []string) []*tcssl.ResourceTypeRegions {\n\tif len(resourceProducts) == 0 || len(resourceRegions) == 0 {\n\t\treturn nil\n\t}\n\n\t// 仅以下云产品类型支持地域\n\tresourceProductsRequireRegion := []string{\"apigateway\", \"clb\", \"cos\", \"tcb\", \"tke\", \"tse\", \"waf\"}\n\n\ttemp := make([]*tcssl.ResourceTypeRegions, 0)\n\tfor _, resourceProduct := range resourceProducts {\n\t\tif slices.Contains(resourceProductsRequireRegion, resourceProduct) {\n\t\t\ttemp = append(temp, &tcssl.ResourceTypeRegions{\n\t\t\t\tResourceType: common.StringPtr(resourceProduct),\n\t\t\t\tRegions:      common.StringPtrs(resourceRegions),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn temp\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-vod/consts.go",
    "content": "package tencentcloudvod\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-vod/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcvod \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod/v20180717\"\n)\n\n// This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/vod/v20180717/client.go\n// to lightweight the vendor packages in the built binary.\ntype VodClient struct {\n\tcommon.Client\n}\n\nfunc NewVodClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *VodClient, err error) {\n\tclient = &VodClient{}\n\tclient.Init(region).\n\t\tWithCredential(credential).\n\t\tWithProfile(clientProfile)\n\treturn\n}\n\nfunc (c *VodClient) DescribeVodDomains(request *tcvod.DescribeVodDomainsRequest) (response *tcvod.DescribeVodDomainsResponse, err error) {\n\treturn c.DescribeVodDomainsWithContext(context.Background(), request)\n}\n\nfunc (c *VodClient) DescribeVodDomainsWithContext(ctx context.Context, request *tcvod.DescribeVodDomainsRequest) (response *tcvod.DescribeVodDomainsResponse, err error) {\n\tif request == nil {\n\t\trequest = tcvod.NewDescribeVodDomainsRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"vod\", tcvod.APIVersion, \"DescribeVodDomains\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeVodDomains require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\n\tresponse = tcvod.NewDescribeVodDomainsResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *VodClient) SetVodDomainCertificate(request *tcvod.SetVodDomainCertificateRequest) (response *tcvod.SetVodDomainCertificateResponse, err error) {\n\treturn c.SetVodDomainCertificateWithContext(context.Background(), request)\n}\n\nfunc (c *VodClient) SetVodDomainCertificateWithContext(ctx context.Context, request *tcvod.SetVodDomainCertificateRequest) (response *tcvod.SetVodDomainCertificateResponse, err error) {\n\tif request == nil {\n\t\trequest = tcvod.NewSetVodDomainCertificateRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"vod\", tcvod.APIVersion, \"SetVodDomainCertificate\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"SetVodDomainCertificate require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcvod.NewSetVodDomainCertificateResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-vod/tencentcloud_vod.go",
    "content": "package tencentcloudvod\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcvod \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod/v20180717\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-vod/internal\"\n)\n\ntype DeployerConfig struct {\n\t// 腾讯云 SecretId。\n\tSecretId string `json:\"secretId\"`\n\t// 腾讯云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 腾讯云接口端点。\n\tEndpoint string `json:\"endpoint,omitempty\"`\n\t// 点播应用 ID。\n\tSubAppId int64 `json:\"subAppId\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 点播加速域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.VodClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tSecretId:  config.SecretId,\n\t\tSecretKey: config.SecretKey,\n\t\tEndpoint: lo.\n\t\t\tIf(strings.HasSuffix(config.Endpoint, \"intl.tencentcloudapi.com\"), \"ssl.intl.tencentcloudapi.com\"). // 国际站使用独立的接口端点\n\t\t\tElse(\"\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的 ECDN 实例\n\tdomains := make([]string, 0)\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = domainCandidates\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no vod domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found vod domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询点播域名列表\n\t// REF: https://cloud.tencent.com/document/api/266/54176\n\tdescribeVodDomainsOffset := 0\n\tdescribeVodDomainsLimit := 20\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeVodDomainsReq := tcvod.NewDescribeVodDomainsRequest()\n\t\tdescribeVodDomainsReq.Offset = common.Uint64Ptr(uint64(describeVodDomainsOffset))\n\t\tdescribeVodDomainsReq.Limit = common.Uint64Ptr(uint64(describeVodDomainsLimit))\n\t\tif d.config.SubAppId != 0 {\n\t\t\tdescribeVodDomainsReq.SubAppId = common.Uint64Ptr(uint64(d.config.SubAppId))\n\t\t}\n\t\tdescribeVodDomainsResp, err := d.sdkClient.DescribeVodDomains(describeVodDomainsReq)\n\t\td.logger.Debug(\"sdk request 'vod.DescribeVodDomains'\", slog.Any(\"request\", describeVodDomainsReq), slog.Any(\"response\", describeVodDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'vod.DescribeVodDomains': %w\", err)\n\t\t}\n\n\t\tif describeVodDomainsResp.Response == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tignoredStatuses := []string{\"Locked\"}\n\t\tfor _, domainItem := range describeVodDomainsResp.Response.DomainSet {\n\t\t\tif lo.Contains(ignoredStatuses, *domainItem.DeployStatus) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdomains = append(domains, *domainItem.Domain)\n\t\t}\n\n\t\tif len(describeVodDomainsResp.Response.DomainSet) < describeVodDomainsLimit {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeVodDomainsOffset += describeVodDomainsLimit\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error {\n\t// 设置点播域名 HTTPS 证书\n\t// REF: https://cloud.tencent.com/document/api/266/102015\n\tsetVodDomainCertificateReq := tcvod.NewSetVodDomainCertificateRequest()\n\tsetVodDomainCertificateReq.Domain = common.StringPtr(domain)\n\tsetVodDomainCertificateReq.Operation = common.StringPtr(\"Set\")\n\tsetVodDomainCertificateReq.CertID = common.StringPtr(cloudCertId)\n\tif d.config.SubAppId != 0 {\n\t\tsetVodDomainCertificateReq.SubAppId = common.Uint64Ptr(uint64(d.config.SubAppId))\n\t}\n\tsetVodDomainCertificateResp, err := d.sdkClient.SetVodDomainCertificate(setVodDomainCertificateReq)\n\td.logger.Debug(\"sdk request 'vod.SetVodDomainCertificate'\", slog.Any(\"request\", setVodDomainCertificateReq), slog.Any(\"response\", setVodDomainCertificateResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'vod.SetVodDomainCertificate': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(secretId, secretKey, endpoint string) (*internal.VodClient, error) {\n\tcredential := common.NewCredential(secretId, secretKey)\n\n\tcpf := profile.NewClientProfile()\n\tif endpoint != \"\" {\n\t\tcpf.HttpProfile.Endpoint = endpoint\n\t}\n\n\tclient, err := internal.NewVodClient(credential, \"\", cpf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-vod/tencentcloud_vod_test.go",
    "content": "package tencentcloudvod_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-vod\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfSecretId      string\n\tfSecretKey     string\n\tfDomain        string\n\tfSubAppId      int64\n\tfInstanceId    string\n)\n\nfunc init() {\n\targsPrefix := \"TENCENTCLOUDVOD_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fSecretId, argsPrefix+\"SECRETID\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n\tflag.Int64Var(&fSubAppId, argsPrefix+\"SUBAPPID\", 0, \"\")\n\tflag.StringVar(&fInstanceId, argsPrefix+\"INSTANCEID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./tencentcloud_vod_test.go -args \\\n\t--TENCENTCLOUDVOD_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--TENCENTCLOUDVOD_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--TENCENTCLOUDVOD_SECRETID=\"your-secret-id\" \\\n\t--TENCENTCLOUDVOD_SECRETKEY=\"your-secret-key\" \\\n\t--TENCENTCLOUDVOD_SUBAPPID=\"your-app-id\" \\\n\t--TENCENTCLOUDVOD_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SECRETID: %v\", fSecretId),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t\tfmt.Sprintf(\"INSTANCEID: %v\", fInstanceId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tSecretId:           fSecretId,\n\t\t\tSecretKey:          fSecretKey,\n\t\t\tSubAppId:           fSubAppId,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-waf/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcwaf \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf/v20180125\"\n)\n\n// This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/waf/v20180125/client.go\n// to lightweight the vendor packages in the built binary.\ntype WafClient struct {\n\tcommon.Client\n}\n\nfunc NewWafClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *WafClient, err error) {\n\tclient = &WafClient{}\n\tclient.Init(region).\n\t\tWithCredential(credential).\n\t\tWithProfile(clientProfile)\n\treturn\n}\n\nfunc (c *WafClient) DescribeDomainDetailsSaas(request *tcwaf.DescribeDomainDetailsSaasRequest) (response *tcwaf.DescribeDomainDetailsSaasResponse, err error) {\n\treturn c.DescribeDomainDetailsSaasWithContext(context.Background(), request)\n}\n\nfunc (c *WafClient) DescribeDomainDetailsSaasWithContext(ctx context.Context, request *tcwaf.DescribeDomainDetailsSaasRequest) (response *tcwaf.DescribeDomainDetailsSaasResponse, err error) {\n\tif request == nil {\n\t\trequest = tcwaf.NewDescribeDomainDetailsSaasRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"waf\", tcwaf.APIVersion, \"DescribeDomainDetailsSaas\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"DescribeDomainDetailsSaas require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcwaf.NewDescribeDomainDetailsSaasResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n\nfunc (c *WafClient) ModifySpartaProtection(request *tcwaf.ModifySpartaProtectionRequest) (response *tcwaf.ModifySpartaProtectionResponse, err error) {\n\treturn c.ModifySpartaProtectionWithContext(context.Background(), request)\n}\n\nfunc (c *WafClient) ModifySpartaProtectionWithContext(ctx context.Context, request *tcwaf.ModifySpartaProtectionRequest) (response *tcwaf.ModifySpartaProtectionResponse, err error) {\n\tif request == nil {\n\t\trequest = tcwaf.NewModifySpartaProtectionRequest()\n\t}\n\tc.InitBaseRequest(&request.BaseRequest, \"waf\", tcwaf.APIVersion, \"ModifySpartaProtection\")\n\n\tif c.GetCredential() == nil {\n\t\treturn nil, errors.New(\"ModifySpartaProtection require credential\")\n\t}\n\n\trequest.SetContext(ctx)\n\tresponse = tcwaf.NewModifySpartaProtectionResponse()\n\terr = c.Send(request, response)\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-waf/tencentcloud_waf.go",
    "content": "package tencentcloudwaf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\ttcwaf \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf/v20180125\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-waf/internal\"\n)\n\ntype DeployerConfig struct {\n\t// 腾讯云 SecretId。\n\tSecretId string `json:\"secretId\"`\n\t// 腾讯云 SecretKey。\n\tSecretKey string `json:\"secretKey\"`\n\t// 腾讯云接口端点。\n\tEndpoint string `json:\"endpoint,omitempty\"`\n\t// 腾讯云地域。\n\tRegion string `json:\"region\"`\n\t// 防护域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n\t// 防护域名 ID。\n\tDomainId string `json:\"domainId\"`\n\t// 防护域名所属实例 ID。\n\tInstanceId string `json:\"instanceId\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.WafClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tSecretId:  config.SecretId,\n\t\tSecretKey: config.SecretKey,\n\t\tEndpoint: lo.\n\t\t\tIf(strings.HasSuffix(config.Endpoint, \"intl.tencentcloudapi.com\"), \"ssl.intl.tencentcloudapi.com\"). // 国际站使用独立的接口端点\n\t\t\tElse(\"\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.Domain == \"\" {\n\t\treturn nil, errors.New(\"config `domain` is required\")\n\t}\n\tif d.config.DomainId == \"\" {\n\t\treturn nil, errors.New(\"config `domainId` is required\")\n\t}\n\tif d.config.InstanceId == \"\" {\n\t\treturn nil, errors.New(\"config `instanceId` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 查询单个 SaaS 型 WAF 域名详情\n\t// REF: https://cloud.tencent.com/document/api/627/82938\n\tdescribeDomainDetailsSaasReq := tcwaf.NewDescribeDomainDetailsSaasRequest()\n\tdescribeDomainDetailsSaasReq.Domain = common.StringPtr(d.config.Domain)\n\tdescribeDomainDetailsSaasReq.DomainId = common.StringPtr(d.config.DomainId)\n\tdescribeDomainDetailsSaasReq.InstanceId = common.StringPtr(d.config.InstanceId)\n\tdescribeDomainDetailsSaasResp, err := d.sdkClient.DescribeDomainDetailsSaas(describeDomainDetailsSaasReq)\n\td.logger.Debug(\"sdk request 'waf.DescribeDomainDetailsSaas'\", slog.Any(\"request\", describeDomainDetailsSaasReq), slog.Any(\"response\", describeDomainDetailsSaasResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'waf.DescribeDomainDetailsSaas': %w\", err)\n\t}\n\n\t// 编辑 SaaS 型 WAF 域名\n\t// REF: https://cloud.tencent.com/document/api/627/94309\n\tmodifySpartaProtectionReq := tcwaf.NewModifySpartaProtectionRequest()\n\tmodifySpartaProtectionReq.Domain = common.StringPtr(d.config.Domain)\n\tmodifySpartaProtectionReq.DomainId = common.StringPtr(d.config.DomainId)\n\tmodifySpartaProtectionReq.InstanceID = common.StringPtr(d.config.InstanceId)\n\tmodifySpartaProtectionReq.CertType = common.Int64Ptr(2)\n\tmodifySpartaProtectionReq.SSLId = common.StringPtr(upres.CertId)\n\tmodifySpartaProtectionResp, err := d.sdkClient.ModifySpartaProtection(modifySpartaProtectionReq)\n\td.logger.Debug(\"sdk request 'waf.ModifySpartaProtection'\", slog.Any(\"request\", modifySpartaProtectionReq), slog.Any(\"response\", modifySpartaProtectionResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'waf.ModifySpartaProtection': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(secretId, secretKey, endpoint, region string) (*internal.WafClient, error) {\n\tcredential := common.NewCredential(secretId, secretKey)\n\n\tcpf := profile.NewClientProfile()\n\tif endpoint != \"\" {\n\t\tcpf.HttpProfile.Endpoint = endpoint\n\t}\n\n\tclient, err := internal.NewWafClient(credential, region, cpf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/tencentcloud-waf/tencentcloud_waf_test.go",
    "content": "package tencentcloudwaf_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-waf\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfSecretId      string\n\tfSecretKey     string\n\tfRegion        string\n\tfDomain        string\n\tfDomainId      string\n\tfInstanceId    string\n)\n\nfunc init() {\n\targsPrefix := \"TENCENTCLOUDWAF_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fSecretId, argsPrefix+\"SECRETID\", \"\", \"\")\n\tflag.StringVar(&fSecretKey, argsPrefix+\"SECRETKEY\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n\tflag.StringVar(&fDomainId, argsPrefix+\"DOMAINID\", \"\", \"\")\n\tflag.StringVar(&fInstanceId, argsPrefix+\"INSTANCEID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./tencentcloud_waf_test.go -args \\\n\t--TENCENTCLOUDWAF_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--TENCENTCLOUDWAF_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--TENCENTCLOUDWAF_SECRETID=\"your-secret-id\" \\\n\t--TENCENTCLOUDWAF_SECRETKEY=\"your-secret-key\" \\\n\t--TENCENTCLOUDWAF_REGION=\"ap-guangzhou\" \\\n\t--TENCENTCLOUDWAF_DOMAIN=\"example.com\" \\\n\t--TENCENTCLOUDWAF_DOMAINID=\"your-domain-id\" \\\n\t--TENCENTCLOUDWAF_INSTANCEID=\"your-instance-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"SECRETID: %v\", fSecretId),\n\t\t\tfmt.Sprintf(\"SECRETKEY: %v\", fSecretKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t\tfmt.Sprintf(\"INSTANCEID: %v\", fInstanceId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tSecretId:   fSecretId,\n\t\t\tSecretKey:  fSecretKey,\n\t\t\tRegion:     fRegion,\n\t\t\tDomain:     fDomain,\n\t\t\tDomainId:   fDomainId,\n\t\t\tInstanceId: fInstanceId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ucloud-ualb/consts.go",
    "content": "package ucloudualb\n\nconst (\n\t// 资源类型：部署到指定负载均衡器。\n\tRESOURCE_TYPE_LOADBALANCER = \"loadbalancer\"\n\t// 资源类型：部署到指定监听器。\n\tRESOURCE_TYPE_LISTENER = \"listener\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/ucloud-ualb/ucloud_ualb.go",
    "content": "package ucloudualb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\t\"github.com/ucloud/ucloud-sdk-go/services/ulb\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ucloud-ulb\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tucloudsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/ulb\"\n)\n\ntype DeployerConfig struct {\n\t// 优刻得 API 私钥。\n\tPrivateKey string `json:\"privateKey\"`\n\t// 优刻得 API 公钥。\n\tPublicKey string `json:\"publicKey\"`\n\t// 优刻得项目 ID。\n\tProjectId string `json:\"projectId,omitempty\"`\n\t// 优刻得地域。\n\tRegion string `json:\"region\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 负载均衡实例 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时必填。\n\tLoadbalancerId string `json:\"loadbalancerId,omitempty\"`\n\t// 负载均衡监听器 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。\n\tListenerId string `json:\"listenerId,omitempty\"`\n\t// SNI 域名（不支持泛域名）。\n\t// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时选填。\n\tDomain string `json:\"domain,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *ucloudsdk.ULBClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tPrivateKey: config.PrivateKey,\n\t\tPublicKey:  config.PublicKey,\n\t\tProjectId:  config.ProjectId,\n\t\tRegion:     config.Region,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_LOADBALANCER:\n\t\tif err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_LISTENER:\n\t\tif err := d.deployToListener(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\n\t// 获取 ALB 下的 HTTPS 监听器列表\n\t// REF: https://docs.ucloud.cn/api/ulb-api/describe_listeners\n\tlistenerIds := make([]string, 0)\n\tdescribeListenersOffset := 0\n\tdescribeListenersLimit := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeListenerReq := d.sdkClient.NewDescribeListenersRequest()\n\t\tdescribeListenerReq.LoadBalancerId = ucloud.String(d.config.LoadbalancerId)\n\t\tdescribeListenerReq.Offset = ucloud.Int(describeListenersOffset)\n\t\tdescribeListenerReq.Limit = ucloud.Int(describeListenersLimit)\n\t\tdescribeListenerResp, err := d.sdkClient.DescribeListeners(describeListenerReq)\n\t\td.logger.Debug(\"sdk request 'ulb.DescribeListeners'\", slog.Any(\"request\", describeListenerReq), slog.Any(\"response\", describeListenerResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'ulb.DescribeListeners': %w\", err)\n\t\t}\n\n\t\tfor _, listenerItem := range describeListenerResp.Listeners {\n\t\t\tif listenerItem.ListenerProtocol == \"HTTPS\" {\n\t\t\t\tlistenerIds = append(listenerIds, listenerItem.ListenerId)\n\t\t\t}\n\t\t}\n\n\t\tif len(describeListenerResp.Listeners) < describeListenersLimit {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeListenersOffset += describeListenersLimit\n\t}\n\n\t// 遍历更新 Listener 证书\n\tif len(listenerIds) == 0 {\n\t\td.logger.Info(\"no alb listeners to deploy\")\n\t} else {\n\t\td.logger.Info(\"found https listeners to deploy\", slog.Any(\"listenerIds\", listenerIds))\n\t\tvar errs []error\n\n\t\tfor _, listenerId := range listenerIds {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listenerId, cloudCertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\tif d.config.ListenerId == \"\" {\n\t\treturn errors.New(\"config `listenerId` is required\")\n\t}\n\n\tif err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, d.config.ListenerId, cloudCertId); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId, cloudListenerId string, cloudCertId string) error {\n\t// 描述应用型负载均衡监听器\n\t// REF: https://docs.ucloud.cn/api/ulb-api/describe_listeners\n\tdescribeListenersReq := d.sdkClient.NewDescribeListenersRequest()\n\tdescribeListenersReq.LoadBalancerId = ucloud.String(cloudLoadbalancerId)\n\tdescribeListenersReq.ListenerId = ucloud.String(cloudListenerId)\n\tdescribeListenersReq.Limit = ucloud.Int(1)\n\tdescribeListenerResp, err := d.sdkClient.DescribeListeners(describeListenersReq)\n\td.logger.Debug(\"sdk request 'ulb.DescribeListeners'\", slog.Any(\"request\", describeListenersReq), slog.Any(\"response\", describeListenerResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'ulb.DescribeListeners': %w\", err)\n\t} else if len(describeListenerResp.Listeners) == 0 {\n\t\treturn fmt.Errorf(\"could not find listener '%s'\", cloudListenerId)\n\t}\n\n\t// 跳过已部署过的监听器\n\tlistenerInfo := describeListenerResp.Listeners[0]\n\tif d.config.Domain == \"\" {\n\t\tif lo.ContainsBy(listenerInfo.Certificates, func(item ulb.Certificate) bool { return item.SSLId == cloudCertId && item.IsDefault }) {\n\t\t\treturn nil\n\t\t}\n\t} else {\n\t\tif lo.ContainsBy(listenerInfo.Certificates, func(item ulb.Certificate) bool { return item.SSLId == cloudCertId && !item.IsDefault }) {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tif d.config.Domain == \"\" {\n\t\t// 未指定 SNI，只需部署到监听器\n\n\t\tupdateListenerAttributeReq := d.sdkClient.NewUpdateListenerAttributeRequest()\n\t\tupdateListenerAttributeReq.LoadBalancerId = ucloud.String(cloudLoadbalancerId)\n\t\tupdateListenerAttributeReq.ListenerId = ucloud.String(cloudListenerId)\n\t\tupdateListenerAttributeReq.Certificates = []string{cloudCertId}\n\t\tupdateListenerResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)\n\t\td.logger.Debug(\"sdk request 'ulb.UpdateListenerAttribute'\", slog.Any(\"request\", updateListenerAttributeReq), slog.Any(\"response\", updateListenerResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'ulb.UpdateListenerAttribute': %w\", err)\n\t\t}\n\t} else {\n\t\t// 指定 SNI，需部署到扩展域名\n\n\t\t// 新增监听器扩展证书\n\t\t// REF: https://docs.ucloud.cn/api/ulb-api/add_ssl_binding_json\n\t\taddSSLBindingReq := d.sdkClient.NewAddSSLBindingRequest()\n\t\taddSSLBindingReq.LoadBalancerId = ucloud.String(cloudLoadbalancerId)\n\t\taddSSLBindingReq.ListenerId = ucloud.String(cloudListenerId)\n\t\taddSSLBindingReq.SSLIds = []string{cloudCertId}\n\t\taddSSLBindingResp, err := d.sdkClient.AddSSLBinding(addSSLBindingReq)\n\t\td.logger.Debug(\"sdk request 'ulb.AddSSLBinding'\", slog.Any(\"request\", addSSLBindingReq), slog.Any(\"response\", addSSLBindingResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'ulb.AddSSLBinding': %w\", err)\n\t\t}\n\n\t\t// 找出需要删除绑定的扩展证书\n\t\t// REF: https://docs.ucloud.cn/api/ulb-api/describe_sslv2\n\t\tsslIdsToDelete := make([]string, 0)\n\t\tfor _, certItem := range listenerInfo.Certificates {\n\t\t\tif certItem.IsDefault {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdescribeSSLV2Req := d.sdkClient.NewDescribeSSLV2Request()\n\t\t\tdescribeSSLV2Req.SSLId = ucloud.String(certItem.SSLId)\n\t\t\tdescribeSSLV2Req.Limit = ucloud.Int(1)\n\t\t\tdescribeSSLV2Resp, err := d.sdkClient.DescribeSSLV2(describeSSLV2Req)\n\t\t\td.logger.Debug(\"sdk request 'ulb.DescribeSSLV2'\", slog.Any(\"request\", describeSSLV2Req), slog.Any(\"response\", describeSSLV2Resp))\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t} else if len(describeSSLV2Resp.DataSet) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsslItem := describeSSLV2Resp.DataSet[0]\n\t\t\tif sslItem.NotAfter != 0 && int64(sslItem.NotAfter) < time.Now().Unix() {\n\t\t\t\tsslIdsToDelete = append(sslIdsToDelete, sslItem.SSLId) // 过期证书需要删除\n\t\t\t\tcontinue\n\t\t\t} else if sslItem.Domains == d.config.Domain {\n\t\t\t\tsslIdsToDelete = append(sslIdsToDelete, sslItem.SSLId) // 同域名证书需要删除\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\t// 删除监听器绑定的扩展证书\n\t\t// REF: https://docs.ucloud.cn/api/ulb-api/delete_ssl_binding_json\n\t\tif len(sslIdsToDelete) > 0 {\n\t\t\tdeleteSSLBindingReq := d.sdkClient.NewDeleteSSLBindingRequest()\n\t\t\tdeleteSSLBindingReq.LoadBalancerId = ucloud.String(cloudLoadbalancerId)\n\t\t\tdeleteSSLBindingReq.ListenerId = ucloud.String(cloudListenerId)\n\t\t\tdeleteSSLBindingReq.SSLIds = sslIdsToDelete\n\t\t\tdeleteSSLBindingResp, err := d.sdkClient.DeleteSSLBinding(deleteSSLBindingReq)\n\t\t\td.logger.Debug(\"sdk request 'ulb.DeleteSSLBinding'\", slog.Any(\"request\", deleteSSLBindingReq), slog.Any(\"response\", deleteSSLBindingResp))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'ulb.DeleteSSLBinding': %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(privateKey, publicKey, projectId, region string) (*ucloudsdk.ULBClient, error) {\n\tif privateKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"ucloud: invalid private key\")\n\t}\n\tif publicKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"ucloud: invalid public key\")\n\t}\n\n\tcfg := ucloud.NewConfig()\n\tcfg.ProjectId = projectId\n\tcfg.Region = region\n\n\tcredential := auth.NewCredential()\n\tcredential.PrivateKey = privateKey\n\tcredential.PublicKey = publicKey\n\n\tclient := ucloudsdk.NewClient(&cfg, &credential)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ucloud-ualb/ucloud_ualb_test.go",
    "content": "package ucloudualb_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-ualb\"\n)\n\nvar (\n\tfInputCertPath  string\n\tfInputKeyPath   string\n\tfPrivateKey     string\n\tfPublicKey      string\n\tfRegion         string\n\tfLoadbalancerId string\n\tfListenerId     string\n)\n\nfunc init() {\n\targsPrefix := \"UCLOUDUALB_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fPrivateKey, argsPrefix+\"PRIVATEKEY\", \"\", \"\")\n\tflag.StringVar(&fPublicKey, argsPrefix+\"PUBLICKEY\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fLoadbalancerId, argsPrefix+\"LOADBALANCERID\", \"\", \"\")\n\tflag.StringVar(&fListenerId, argsPrefix+\"LISTENERID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ucloud_ualb_test.go -args \\\n\t--UCLOUDUALB_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--UCLOUDUALB_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--UCLOUDUALB_PRIVATEKEY=\"your-private-key\" \\\n\t--UCLOUDUALB_PUBLICKEY=\"your-public-key\" \\\n\t--UCLOUDUALB_REGION=\"cn-bj2\" \\\n\t--UCLOUDUALB_LOADBALANCERID=\"your-loadbalancer-id\" \\\n\t--UCLOUDUALB_LISTENERID=\"your-listener-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"PRIVATEKEY: %v\", fPrivateKey),\n\t\t\tfmt.Sprintf(\"PUBLICKEY: %v\", fPublicKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LOADBALANCERID: %v\", fLoadbalancerId),\n\t\t\tfmt.Sprintf(\"LISTENERID: %v\", fListenerId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tPrivateKey:     fPrivateKey,\n\t\t\tPublicKey:      fPublicKey,\n\t\t\tRegion:         fRegion,\n\t\t\tResourceType:   provider.RESOURCE_TYPE_LISTENER,\n\t\t\tLoadbalancerId: fLoadbalancerId,\n\t\t\tListenerId:     fListenerId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ucloud-ucdn/ucloud_ucdn.go",
    "content": "package uclouducdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ucloud-ussl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tucloudsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/ucdn\"\n)\n\ntype DeployerConfig struct {\n\t// 优刻得 API 私钥。\n\tPrivateKey string `json:\"privateKey\"`\n\t// 优刻得 API 公钥。\n\tPublicKey string `json:\"publicKey\"`\n\t// 优刻得项目 ID。\n\tProjectId string `json:\"projectId,omitempty\"`\n\t// 加速域名 ID。\n\tDomainId string `json:\"domainId\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *ucloudsdk.UCDNClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tPrivateKey: config.PrivateKey,\n\t\tPublicKey:  config.PublicKey,\n\t\tProjectId:  config.ProjectId,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.DomainId == \"\" {\n\t\treturn nil, errors.New(\"config `domainId` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取加速域名配置\n\t// REF: https://docs.ucloud.cn/api/ucdn-api/get_ucdn_domain_config\n\tgetUcdnDomainConfigReq := d.sdkClient.NewGetUcdnDomainConfigRequest()\n\tgetUcdnDomainConfigReq.DomainId = []string{d.config.DomainId}\n\tgetUcdnDomainConfigResp, err := d.sdkClient.GetUcdnDomainConfig(getUcdnDomainConfigReq)\n\td.logger.Debug(\"sdk request 'ucdn.GetUcdnDomainConfig'\", slog.Any(\"request\", getUcdnDomainConfigReq), slog.Any(\"response\", getUcdnDomainConfigResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'ucdn.GetUcdnDomainConfig': %w\", err)\n\t} else if len(getUcdnDomainConfigResp.DomainList) == 0 {\n\t\treturn nil, fmt.Errorf(\"could not find domain '%s'\", d.config.DomainId)\n\t}\n\n\t// 更新 HTTPS 加速配置\n\t// REF: https://docs.ucloud.cn/api/ucdn-api/update_ucdn_domain_https_config_v2\n\tcertId, _ := strconv.Atoi(upres.CertId)\n\tupdateUcdnDomainHttpsConfigV2Req := d.sdkClient.NewUpdateUcdnDomainHttpsConfigV2Request()\n\tupdateUcdnDomainHttpsConfigV2Req.DomainId = ucloud.String(d.config.DomainId)\n\tupdateUcdnDomainHttpsConfigV2Req.HttpsStatusCn = ucloud.String(getUcdnDomainConfigResp.DomainList[0].HttpsStatusCn)\n\tupdateUcdnDomainHttpsConfigV2Req.HttpsStatusAbroad = ucloud.String(getUcdnDomainConfigResp.DomainList[0].HttpsStatusAbroad)\n\tupdateUcdnDomainHttpsConfigV2Req.HttpsStatusAbroad = ucloud.String(getUcdnDomainConfigResp.DomainList[0].HttpsStatusAbroad)\n\tupdateUcdnDomainHttpsConfigV2Req.CertId = ucloud.Int(certId)\n\tupdateUcdnDomainHttpsConfigV2Req.CertName = ucloud.String(upres.CertName)\n\tupdateUcdnDomainHttpsConfigV2Req.CertType = ucloud.String(\"ussl\")\n\tupdateUcdnDomainHttpsConfigV2Resp, err := d.sdkClient.UpdateUcdnDomainHttpsConfigV2(updateUcdnDomainHttpsConfigV2Req)\n\td.logger.Debug(\"sdk request 'ucdn.UpdateUcdnDomainHttpsConfigV2'\", slog.Any(\"request\", updateUcdnDomainHttpsConfigV2Req), slog.Any(\"response\", updateUcdnDomainHttpsConfigV2Resp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'ucdn.UpdateUcdnDomainHttpsConfigV2': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(privateKey, publicKey, projectId string) (*ucloudsdk.UCDNClient, error) {\n\tif privateKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"ucloud: invalid private key\")\n\t}\n\tif publicKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"ucloud: invalid public key\")\n\t}\n\n\tcfg := ucloud.NewConfig()\n\tcfg.ProjectId = projectId\n\n\tcredential := auth.NewCredential()\n\tcredential.PrivateKey = privateKey\n\tcredential.PublicKey = publicKey\n\n\tclient := ucloudsdk.NewClient(&cfg, &credential)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ucloud-ucdn/ucloud_ucdn_test.go",
    "content": "package uclouducdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-ucdn\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfPrivateKey    string\n\tfPublicKey     string\n\tfDomainId      string\n)\n\nfunc init() {\n\targsPrefix := \"UCLOUDUCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fPrivateKey, argsPrefix+\"PRIVATEKEY\", \"\", \"\")\n\tflag.StringVar(&fPublicKey, argsPrefix+\"PUBLICKEY\", \"\", \"\")\n\tflag.StringVar(&fDomainId, argsPrefix+\"DOMAINID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ucloud_ucdn_test.go -args \\\n\t--UCLOUDUCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--UCLOUDUCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--UCLOUDUCDN_PRIVATEKEY=\"your-private-key\" \\\n\t--UCLOUDUCDN_PUBLICKEY=\"your-public-key\" \\\n\t--UCLOUDUCDN_DOMAINID=\"your-domain-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"PRIVATEKEY: %v\", fPrivateKey),\n\t\t\tfmt.Sprintf(\"PUBLICKEY: %v\", fPublicKey),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomainId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tPrivateKey: fPrivateKey,\n\t\t\tPublicKey:  fPublicKey,\n\t\t\tDomainId:   fDomainId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ucloud-uclb/consts.go",
    "content": "package uclouduclb\n\nconst (\n\t// 资源类型：部署到指定负载均衡器。\n\tRESOURCE_TYPE_LOADBALANCER = \"loadbalancer\"\n\t// 资源类型：部署到指定 VServer。\n\tRESOURCE_TYPE_VSERVER = \"vserver\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/ucloud-uclb/ucloud_uclb.go",
    "content": "package uclouduclb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sync\"\n\n\t\"github.com/samber/lo\"\n\t\"github.com/ucloud/ucloud-sdk-go/services/ulb\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ucloud-ulb\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tucloudsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/ulb\"\n)\n\ntype DeployerConfig struct {\n\t// 优刻得 API 私钥。\n\tPrivateKey string `json:\"privateKey\"`\n\t// 优刻得 API 公钥。\n\tPublicKey string `json:\"publicKey\"`\n\t// 优刻得项目 ID。\n\tProjectId string `json:\"projectId,omitempty\"`\n\t// 优刻得地域。\n\tRegion string `json:\"region\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 负载均衡实例 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_VSERVER] 时必填。\n\tLoadbalancerId string `json:\"loadbalancerId,omitempty\"`\n\t// 负载均衡 VServer ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_VSERVER] 时必填。\n\tVServerId string `json:\"vserverId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *ucloudsdk.ULBClient\n\tsdkCertmgr certmgr.Provider\n\n\tsslId2PemMap   map[string]string\n\tsslId2PemMapMu sync.Mutex\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tPrivateKey: config.PrivateKey,\n\t\tPublicKey:  config.PublicKey,\n\t\tProjectId:  config.ProjectId,\n\t\tRegion:     config.Region,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\n\t\tsslId2PemMap:   make(map[string]string),\n\t\tsslId2PemMapMu: sync.Mutex{},\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\n\t\td.sslId2PemMapMu.Lock()\n\t\td.sslId2PemMap[upres.CertId] = certPEM\n\t\td.sslId2PemMapMu.Unlock()\n\t}\n\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_LOADBALANCER:\n\t\tif err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_VSERVER:\n\t\tif err := d.deployToVServer(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\n\t// 获取 CLB 下的 HTTPS VServer 列表\n\t// REF: https://docs.ucloud.cn/api/ulb-api/describe_vserver\n\tvserverIds := make([]string, 0)\n\tdescribeVServerOffset := 0\n\tdescribeVServerLimit := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeVServerReq := d.sdkClient.NewDescribeVServerRequest()\n\t\tdescribeVServerReq.ULBId = ucloud.String(d.config.LoadbalancerId)\n\t\tdescribeVServerReq.Offset = ucloud.Int(describeVServerOffset)\n\t\tdescribeVServerReq.Limit = ucloud.Int(describeVServerLimit)\n\t\tdescribeVServerResp, err := d.sdkClient.DescribeVServer(describeVServerReq)\n\t\td.logger.Debug(\"sdk request 'ulb.DescribeVServer'\", slog.Any(\"request\", describeVServerReq), slog.Any(\"response\", describeVServerResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'ulb.DescribeVServer': %w\", err)\n\t\t}\n\n\t\tfor _, vserverItem := range describeVServerResp.DataSet {\n\t\t\tif vserverItem.Protocol == \"HTTPS\" {\n\t\t\t\tvserverIds = append(vserverIds, vserverItem.VServerId)\n\t\t\t}\n\t\t}\n\n\t\tif len(describeVServerResp.DataSet) < describeVServerLimit {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeVServerOffset += describeVServerLimit\n\t}\n\n\t// 遍历更新 VServer 证书\n\tif len(vserverIds) == 0 {\n\t\td.logger.Info(\"no clb vservers to deploy\")\n\t} else {\n\t\td.logger.Info(\"found https vservers to deploy\", slog.Any(\"vserverIds\", vserverIds))\n\t\tvar errs []error\n\n\t\tfor _, vserverId := range vserverIds {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateVServerCertificate(ctx, d.config.LoadbalancerId, vserverId, cloudCertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToVServer(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\tif d.config.VServerId == \"\" {\n\t\treturn errors.New(\"config `vserverId` is required\")\n\t}\n\n\tif err := d.updateVServerCertificate(ctx, d.config.LoadbalancerId, d.config.VServerId, cloudCertId); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateVServerCertificate(ctx context.Context, cloudLoadbalancerId, cloudVServerId string, cloudCertId string) error {\n\t// 获取 CLB 下的 VServer 信息\n\t// REF: https://docs.ucloud.cn/api/ulb-api/describe_vserver\n\tdescribeVServerReq := d.sdkClient.NewDescribeVServerRequest()\n\tdescribeVServerReq.ULBId = ucloud.String(cloudLoadbalancerId)\n\tdescribeVServerReq.VServerId = ucloud.String(cloudVServerId)\n\tdescribeVServerReq.Limit = ucloud.Int(1)\n\tdescribeVServerResp, err := d.sdkClient.DescribeVServer(describeVServerReq)\n\td.logger.Debug(\"sdk request 'ulb.DescribeVServer'\", slog.Any(\"request\", describeVServerReq), slog.Any(\"response\", describeVServerResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'ulb.DescribeVServer': %w\", err)\n\t} else if len(describeVServerResp.DataSet) == 0 {\n\t\treturn fmt.Errorf(\"could not find vserver '%s'\", cloudVServerId)\n\t}\n\n\t// 跳过已部署过的 VServer\n\tvserverInfo := describeVServerResp.DataSet[0]\n\tif lo.ContainsBy(vserverInfo.SSLSet, func(item ulb.ULBSSLSet) bool { return item.SSLId == cloudCertId }) {\n\t\treturn nil\n\t}\n\n\t// 解绑 SSL 证书\n\t// REF: https://docs.ucloud.cn/api/ulb-api/unbind_ssl\n\t//\n\t// 注意，虽然文档中描述为数组结构，但实际 VServer 最多只允许绑定一个证书，因此需要先解绑旧证书才能绑定新证书\n\t// https://github.com/certimate-go/certimate/issues/1224\n\tfor _, sslItem := range vserverInfo.SSLSet {\n\t\tunbindSSLReq := d.sdkClient.NewUnbindSSLRequest()\n\t\tunbindSSLReq.ULBId = ucloud.String(cloudLoadbalancerId)\n\t\tunbindSSLReq.VServerId = ucloud.String(cloudVServerId)\n\t\tunbindSSLReq.SSLId = ucloud.String(sslItem.SSLId)\n\t\tunbindSSLResp, err := d.sdkClient.UnbindSSL(unbindSSLReq)\n\t\td.logger.Debug(\"sdk request 'ulb.UnbindSSL'\", slog.Any(\"request\", unbindSSLReq), slog.Any(\"response\", unbindSSLResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'ulb.UnbindSSL': %w\", err)\n\t\t}\n\t}\n\n\t// 绑定 SSL 证书\n\t// REF: https://docs.ucloud.cn/api/ulb-api/bind_ssl\n\tbindSSLReq := d.sdkClient.NewBindSSLRequest()\n\tbindSSLReq.ULBId = ucloud.String(cloudLoadbalancerId)\n\tbindSSLReq.VServerId = ucloud.String(cloudVServerId)\n\tbindSSLReq.SSLId = ucloud.String(cloudCertId)\n\tbindSSLResp, err := d.sdkClient.BindSSL(bindSSLReq)\n\td.logger.Debug(\"sdk request 'ulb.BindSSL'\", slog.Any(\"request\", bindSSLReq), slog.Any(\"response\", bindSSLResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'ulb.BindSSL': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(privateKey, publicKey, projectId, region string) (*ucloudsdk.ULBClient, error) {\n\tif privateKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"ucloud: invalid private key\")\n\t}\n\tif publicKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"ucloud: invalid public key\")\n\t}\n\n\tcfg := ucloud.NewConfig()\n\tcfg.ProjectId = projectId\n\tcfg.Region = region\n\n\tcredential := auth.NewCredential()\n\tcredential.PrivateKey = privateKey\n\tcredential.PublicKey = publicKey\n\n\tclient := ucloudsdk.NewClient(&cfg, &credential)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ucloud-uclb/ucloud_uclb_test.go",
    "content": "package uclouduclb_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-uclb\"\n)\n\nvar (\n\tfInputCertPath  string\n\tfInputKeyPath   string\n\tfPrivateKey     string\n\tfPublicKey      string\n\tfRegion         string\n\tfLoadbalancerId string\n\tfVServerId      string\n)\n\nfunc init() {\n\targsPrefix := \"UCLOUDUCLB_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fPrivateKey, argsPrefix+\"PRIVATEKEY\", \"\", \"\")\n\tflag.StringVar(&fPublicKey, argsPrefix+\"PUBLICKEY\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fLoadbalancerId, argsPrefix+\"LOADBALANCERID\", \"\", \"\")\n\tflag.StringVar(&fVServerId, argsPrefix+\"VSERVERID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ucloud_uclb_test.go -args \\\n\t--UCLOUDUCLB_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--UCLOUDUCLB_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--UCLOUDUCLB_PRIVATEKEY=\"your-private-key\" \\\n\t--UCLOUDUCLB_PUBLICKEY=\"your-public-key\" \\\n\t--UCLOUDUCLB_REGION=\"cn-bj2\" \\\n\t--UCLOUDUCLB_LOADBALANCERID=\"your-loadbalancer-id\" \\\n\t--UCLOUDUCLB_VSERVERID=\"your-vserver-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"PRIVATEKEY: %v\", fPrivateKey),\n\t\t\tfmt.Sprintf(\"PUBLICKEY: %v\", fPublicKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LOADBALANCERID: %v\", fLoadbalancerId),\n\t\t\tfmt.Sprintf(\"VSERVERID: %v\", fVServerId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tPrivateKey:     fPrivateKey,\n\t\t\tPublicKey:      fPublicKey,\n\t\t\tRegion:         fRegion,\n\t\t\tResourceType:   provider.RESOURCE_TYPE_VSERVER,\n\t\t\tLoadbalancerId: fLoadbalancerId,\n\t\t\tVServerId:      fVServerId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ucloud-uewaf/ucloud_uewaf.go",
    "content": "package uclouduewaf\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tucloudsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/uewaf\"\n)\n\ntype DeployerConfig struct {\n\t// 优刻得 API 私钥。\n\tPrivateKey string `json:\"privateKey\"`\n\t// 优刻得 API 公钥。\n\tPublicKey string `json:\"publicKey\"`\n\t// 优刻得项目 ID。\n\tProjectId string `json:\"projectId,omitempty\"`\n\t// 自定义域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *ucloudsdk.UEWAFClient\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.Domain == \"\" {\n\t\treturn nil, errors.New(\"config `domain` is required\")\n\t}\n\n\t// 生成优刻得所需的证书参数\n\tcertPEMBase64 := base64.StdEncoding.EncodeToString([]byte(certPEM))\n\tprivkeyPEMBase64 := base64.StdEncoding.EncodeToString([]byte(privkeyPEM))\n\tcertMd5 := md5.Sum([]byte(certPEMBase64 + privkeyPEMBase64))\n\tcertMd5Hex := hex.EncodeToString(certMd5[:])\n\tcertName := fmt.Sprintf(\"certimate_%d\", time.Now().UnixMilli())\n\n\t// 添加 SSL 证书\n\t// REF: https://docs.ucloud.cn/api/uewaf-api/add_waf_domain_certificate_info\n\taddWafDomainCertificateInfoReq := d.sdkClient.NewAddWafDomainCertificateInfoRequest()\n\taddWafDomainCertificateInfoReq.Domain = ucloud.String(d.config.Domain)\n\taddWafDomainCertificateInfoReq.CertificateName = ucloud.String(certName)\n\taddWafDomainCertificateInfoReq.SslPublicKey = ucloud.String(certPEMBase64)\n\taddWafDomainCertificateInfoReq.SslPrivateKey = ucloud.String(privkeyPEMBase64)\n\taddWafDomainCertificateInfoReq.SslMD = ucloud.String(certMd5Hex)\n\taddWafDomainCertificateInfoResp, err := d.sdkClient.AddWafDomainCertificateInfo(addWafDomainCertificateInfoReq)\n\td.logger.Debug(\"sdk request 'uewaf.AddWafDomainCertificateInfo'\", slog.Any(\"request\", addWafDomainCertificateInfoReq), slog.Any(\"response\", addWafDomainCertificateInfoResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'uewaf.AddWafDomainCertificateInfo': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(privateKey, publicKey, projectId string) (*ucloudsdk.UEWAFClient, error) {\n\tif privateKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"ucloud: invalid private key\")\n\t}\n\tif publicKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"ucloud: invalid public key\")\n\t}\n\n\tcfg := ucloud.NewConfig()\n\tcfg.ProjectId = projectId\n\n\tcredential := auth.NewCredential()\n\tcredential.PrivateKey = privateKey\n\tcredential.PublicKey = publicKey\n\n\tclient := ucloudsdk.NewClient(&cfg, &credential)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ucloud-uewaf/ucloud_uewaf_test.go",
    "content": "package uclouduewaf_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-uewaf\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfPrivateKey    string\n\tfPublicKey     string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"UCLOUDUEWAF_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fPrivateKey, argsPrefix+\"PRIVATEKEY\", \"\", \"\")\n\tflag.StringVar(&fPublicKey, argsPrefix+\"PUBLICKEY\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ucloud_uewaf_test.go -args \\\n\t--UCLOUDUEWAF_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--UCLOUDUEWAF_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--UCLOUDUEWAF_PRIVATEKEY=\"your-private-key\" \\\n\t--UCLOUDUEWAF_PUBLICKEY=\"your-public-key\" \\\n\t--UCLOUDUEWAF_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"PRIVATEKEY: %v\", fPrivateKey),\n\t\t\tfmt.Sprintf(\"PUBLICKEY: %v\", fPublicKey),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tPrivateKey: fPrivateKey,\n\t\t\tPublicKey:  fPublicKey,\n\t\t\tDomain:     fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ucloud-upathx/ucloud_upathx.go",
    "content": "package ucloudupathx\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/ucloud/ucloud-sdk-go/services/uaccount\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ucloud-upathx\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tucloudsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/upathx\"\n)\n\ntype DeployerConfig struct {\n\t// 优刻得 API 私钥。\n\tPrivateKey string `json:\"privateKey\"`\n\t// 优刻得 API 公钥。\n\tPublicKey string `json:\"publicKey\"`\n\t// 优刻得项目 ID。\n\tProjectId string `json:\"projectId,omitempty\"`\n\t// 加速器实例 ID。\n\tAcceleratorId string `json:\"acceleratorId\"`\n\t// 加速器监听端口。\n\tListenerPort int32 `json:\"listenerPort\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *ucloudsdk.UPathXClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tPrivateKey: config.PrivateKey,\n\t\tPublicKey:  config.PublicKey,\n\t\tProjectId:  config.ProjectId,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.AcceleratorId == \"\" {\n\t\treturn nil, errors.New(\"config `acceleratorId` is required\")\n\t}\n\tif d.config.ListenerPort == 0 {\n\t\treturn nil, errors.New(\"config `listenerPort` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 绑定 PathX SSL 证书\n\t// REF: https://docs.ucloud.cn/api/pathx-api/bind_path_xssl\n\tbindPathXSSLReq := d.sdkClient.NewBindPathXSSLRequest()\n\tbindPathXSSLReq.UGAId = ucloud.String(d.config.AcceleratorId)\n\tbindPathXSSLReq.Port = []int{int(d.config.ListenerPort)}\n\tbindPathXSSLReq.SSLId = ucloud.String(upres.CertId)\n\tbindPathXSSLResp, err := d.sdkClient.BindPathXSSL(bindPathXSSLReq)\n\td.logger.Debug(\"sdk request 'pathx.BindPathXSSL'\", slog.Any(\"request\", bindPathXSSLReq), slog.Any(\"response\", bindPathXSSLResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'pathx.BindPathXSSL': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(privateKey, publicKey, projectId string) (*ucloudsdk.UPathXClient, error) {\n\tif privateKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"ucloud: invalid private key\")\n\t}\n\tif publicKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"ucloud: invalid public key\")\n\t}\n\n\tcfg := ucloud.NewConfig()\n\tcfg.ProjectId = projectId\n\n\t// PathX 相关接口要求必传 ProjectId 参数\n\tif cfg.ProjectId == \"\" {\n\t\tdefaultProjectId, err := getSDKDefaultProjectId(privateKey, publicKey)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tcfg.ProjectId = defaultProjectId\n\t}\n\n\tcredential := auth.NewCredential()\n\tcredential.PrivateKey = privateKey\n\tcredential.PublicKey = publicKey\n\n\tclient := ucloudsdk.NewClient(&cfg, &credential)\n\treturn client, nil\n}\n\nfunc getSDKDefaultProjectId(privateKey, publicKey string) (string, error) {\n\tcfg := ucloud.NewConfig()\n\n\tcredential := auth.NewCredential()\n\tcredential.PrivateKey = privateKey\n\tcredential.PublicKey = publicKey\n\n\tclient := uaccount.NewClient(&cfg, &credential)\n\n\trequest := client.NewGetProjectListRequest()\n\tresponse, err := client.GetProjectList(request)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, projectItem := range response.ProjectSet {\n\t\tif projectItem.IsDefault {\n\t\t\treturn projectItem.ProjectId, nil\n\t\t}\n\t}\n\n\treturn \"\", errors.New(\"ucloud: no default project found\")\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ucloud-upathx/ucloud_upathx_test.go",
    "content": "package ucloudupathx_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-upathx\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfPrivateKey    string\n\tfPublicKey     string\n\tfRegion        string\n\tfAcceleratorId string\n\tfListenerPort  int\n)\n\nfunc init() {\n\targsPrefix := \"UCLOUDUPATHX_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fPrivateKey, argsPrefix+\"PRIVATEKEY\", \"\", \"\")\n\tflag.StringVar(&fPublicKey, argsPrefix+\"PUBLICKEY\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fAcceleratorId, argsPrefix+\"ACCELERATORID\", \"\", \"\")\n\tflag.IntVar(&fListenerPort, argsPrefix+\"LISTENERPORT\", 443, \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ucloud_upathx_test.go -args \\\n\t--UCLOUDUPATHX_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--UCLOUDUPATHX_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--UCLOUDUPATHX_PRIVATEKEY=\"your-private-key\" \\\n\t--UCLOUDUPATHX_PUBLICKEY=\"your-public-key\" \\\n\t--UCLOUDUPATHX_ACCELERATORID=\"your-uga-id\" \\\n\t--UCLOUDUPATHX_ACCELERATORPORT=\"443\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"PRIVATEKEY: %v\", fPrivateKey),\n\t\t\tfmt.Sprintf(\"PUBLICKEY: %v\", fPublicKey),\n\t\t\tfmt.Sprintf(\"ACCELERATORID: %v\", fAcceleratorId),\n\t\t\tfmt.Sprintf(\"LISTENERPORT: %v\", fListenerPort),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tPrivateKey:    fPrivateKey,\n\t\t\tPublicKey:     fPublicKey,\n\t\t\tAcceleratorId: fAcceleratorId,\n\t\t\tListenerPort:  int32(fListenerPort),\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ucloud-us3/ucloud_us3.go",
    "content": "package ucloudus3\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/ucloud-ussl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tucloudsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/ufile\"\n)\n\ntype DeployerConfig struct {\n\t// 优刻得 API 私钥。\n\tPrivateKey string `json:\"privateKey\"`\n\t// 优刻得 API 公钥。\n\tPublicKey string `json:\"publicKey\"`\n\t// 优刻得项目 ID。\n\tProjectId string `json:\"projectId,omitempty\"`\n\t// 优刻得地域。\n\tRegion string `json:\"region\"`\n\t// 存储桶名。\n\tBucket string `json:\"bucket\"`\n\t// 自定义域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *ucloudsdk.UFileClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tPrivateKey: config.PrivateKey,\n\t\tPublicKey:  config.PublicKey,\n\t\tProjectId:  config.ProjectId,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.Bucket == \"\" {\n\t\treturn nil, errors.New(\"config `bucket` is required\")\n\t}\n\tif d.config.Domain == \"\" {\n\t\treturn nil, errors.New(\"config `domain` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 添加 SSL 证书\n\t// REF: https://docs.ucloud.cn/api/ufile-api/add_ufile_ssl_cert\n\taddUFileSSLCertReq := d.sdkClient.NewAddUFileSSLCertRequest()\n\taddUFileSSLCertReq.BucketName = ucloud.String(d.config.Bucket)\n\taddUFileSSLCertReq.Domain = ucloud.String(d.config.Domain)\n\taddUFileSSLCertReq.USSLId = ucloud.String(upres.CertId)\n\taddUFileSSLCertReq.CertificateName = ucloud.String(upres.CertName)\n\taddUFileSSLCertResp, err := d.sdkClient.AddUFileSSLCert(addUFileSSLCertReq)\n\td.logger.Debug(\"sdk request 'us3.AddUFileSSLCert'\", slog.Any(\"request\", addUFileSSLCertReq), slog.Any(\"response\", addUFileSSLCertResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'us3.AddUFileSSLCert': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(privateKey, publicKey, projectId, region string) (*ucloudsdk.UFileClient, error) {\n\tif privateKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"ucloud: invalid private key\")\n\t}\n\tif publicKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"ucloud: invalid public key\")\n\t}\n\n\tcfg := ucloud.NewConfig()\n\tcfg.ProjectId = projectId\n\tcfg.Region = region\n\n\tcredential := auth.NewCredential()\n\tcredential.PrivateKey = privateKey\n\tcredential.PublicKey = publicKey\n\n\tclient := ucloudsdk.NewClient(&cfg, &credential)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/ucloud-us3/ucloud_us3_test.go",
    "content": "package ucloudus3_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-us3\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfPrivateKey    string\n\tfPublicKey     string\n\tfRegion        string\n\tfBucket        string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"UCLOUDUS3_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fPrivateKey, argsPrefix+\"PRIVATEKEY\", \"\", \"\")\n\tflag.StringVar(&fPublicKey, argsPrefix+\"PUBLICKEY\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fBucket, argsPrefix+\"BUCKET\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./ucloud_us3_test.go -args \\\n\t--UCLOUDUS3_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--UCLOUDUS3_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--UCLOUDUS3_PRIVATEKEY=\"your-private-key\" \\\n\t--UCLOUDUS3_PUBLICKEY=\"your-public-key\" \\\n\t--UCLOUDUS3_REGION=\"cn-bj2\" \\\n\t--UCLOUDUS3_BUCKET=\"your-us3-bucket\" \\\n\t--UCLOUDUS3_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"PRIVATEKEY: %v\", fPrivateKey),\n\t\t\tfmt.Sprintf(\"PUBLICKEY: %v\", fPublicKey),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"BUCKET: %v\", fBucket),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tPrivateKey: fPrivateKey,\n\t\t\tPublicKey:  fPublicKey,\n\t\t\tRegion:     fRegion,\n\t\t\tBucket:     fBucket,\n\t\t\tDomain:     fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/unicloud-webhost/unicloud_webhost.go",
    "content": "package unicloudwebhost\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/url\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tunicloudsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/dcloud/unicloud\"\n)\n\ntype DeployerConfig struct {\n\t// uniCloud 控制台账号。\n\tUsername string `json:\"username\"`\n\t// uniCloud 控制台密码。\n\tPassword string `json:\"password\"`\n\t// 服务空间提供商。\n\t// 可取值 \"aliyun\"、\"tencent\"。\n\tSpaceProvider string `json:\"spaceProvider\"`\n\t// 服务空间 ID。\n\tSpaceId string `json:\"spaceId\"`\n\t// 托管网站域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *unicloudsdk.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.Username, config.Password)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.SpaceProvider == \"\" {\n\t\treturn nil, errors.New(\"config `spaceProvider` is required\")\n\t}\n\tif d.config.SpaceId == \"\" {\n\t\treturn nil, errors.New(\"config `spaceId` is required\")\n\t}\n\tif d.config.Domain == \"\" {\n\t\treturn nil, errors.New(\"config `domain` is required\")\n\t}\n\n\t// 变更网站证书\n\tcreateDomainWithCertReq := &unicloudsdk.CreateDomainWithCertRequest{\n\t\tProvider: d.config.SpaceProvider,\n\t\tSpaceId:  d.config.SpaceId,\n\t\tDomain:   d.config.Domain,\n\t\tCert:     url.QueryEscape(certPEM),\n\t\tKey:      url.QueryEscape(privkeyPEM),\n\t}\n\tcreateDomainWithCertResp, err := d.sdkClient.CreateDomainWithCert(createDomainWithCertReq)\n\td.logger.Debug(\"sdk request 'unicloud.host.CreateDomainWithCert'\", slog.Any(\"request\", createDomainWithCertReq), slog.Any(\"response\", createDomainWithCertResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'unicloud.host.CreateDomainWithCert': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(username, password string) (*unicloudsdk.Client, error) {\n\treturn unicloudsdk.NewClient(username, password)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/unicloud-webhost/unicloud_webhost_test.go",
    "content": "package unicloudwebhost_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/unicloud-webhost\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfUsername      string\n\tfPassword      string\n\tfSpaceProvider string\n\tfSpaceId       string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"UNICLOUDWEBHOST_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fUsername, argsPrefix+\"USERNAME\", \"\", \"\")\n\tflag.StringVar(&fPassword, argsPrefix+\"PASSWORD\", \"\", \"\")\n\tflag.StringVar(&fSpaceProvider, argsPrefix+\"SPACEPROVIDER\", \"\", \"\")\n\tflag.StringVar(&fSpaceId, argsPrefix+\"SPACEID\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./unicloud_webhost_test.go -args \\\n\t--UNICLOUDWEBHOST_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--UNICLOUDWEBHOST_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--UNICLOUDWEBHOST_USERNAME=\"your-username\" \\\n\t--UNICLOUDWEBHOST_PASSWORD=\"your-password\" \\\n\t--UNICLOUDWEBHOST_SPACEPROVIDER=\"aliyun/tencent\" \\\n\t--UNICLOUDWEBHOST_SPACEID=\"your-space-id\" \\\n\t--UNICLOUDWEBHOST_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"USERNAME: %v\", fUsername),\n\t\t\tfmt.Sprintf(\"PASSWORD: %v\", fPassword),\n\t\t\tfmt.Sprintf(\"SPACEPROVIDER: %v\", fSpaceProvider),\n\t\t\tfmt.Sprintf(\"SPACEID: %v\", fSpaceId),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tUsername:      fUsername,\n\t\t\tPassword:      fPassword,\n\t\t\tSpaceProvider: fSpaceProvider,\n\t\t\tSpaceId:       fSpaceId,\n\t\t\tDomain:        fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/upyun-cdn/consts.go",
    "content": "package upyuncdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/upyun-cdn/upyun_cdn.go",
    "content": "package upyuncdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/upyun-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tupyunsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/upyun/console\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 又拍云账号用户名。\n\tUsername string `json:\"username\"`\n\t// 又拍云账号密码。\n\tPassword string `json:\"password\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *upyunsdk.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.Username, config.Password)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tUsername: config.Username,\n\t\tPassword: config.Password,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tvar domains []string\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain)\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no cdn domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found cdn domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 获取服务列表\n\tgetBucketsPage := 1\n\tgetBucketsPerPage := 10\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tgetBucketsReq := &upyunsdk.GetBucketsRequest{\n\t\t\tType:          \"ucdn\",\n\t\t\tTag:           \"all\",\n\t\t\tStatus:        \"all\",\n\t\t\tIsSecurityCDN: false,\n\t\t\tWithDomains:   true,\n\t\t\tPage:          int32(getBucketsPage),\n\t\t\tPerPage:       int32(getBucketsPerPage),\n\t\t}\n\t\tgetBucketsResp, err := d.sdkClient.GetBucketsWithContext(ctx, getBucketsReq)\n\t\td.logger.Debug(\"sdk request 'console.GetBuckets'\", slog.Any(\"request\", getBucketsReq), slog.Any(\"response\", getBucketsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'console.GetBuckets': %w\", err)\n\t\t}\n\n\t\tif getBucketsResp.Data == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, bucketItem := range getBucketsResp.Data.Buckets {\n\t\t\tif !bucketItem.Visible {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfor _, domainItem := range bucketItem.Domains {\n\t\t\t\tif strings.EqualFold(domainItem.Status, \"NORMAL\") && !strings.HasSuffix(domainItem.Domain, \".test.upcdn.net\") {\n\t\t\t\t\tdomains = append(domains, domainItem.Domain)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(getBucketsResp.Data.Buckets) < getBucketsPerPage {\n\t\t\tbreak\n\t\t}\n\n\t\tgetBucketsPage++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error {\n\t// 获取域名证书配置\n\tgetHttpsServiceManagerResp, err := d.sdkClient.GetHttpsServiceManagerWithContext(ctx, domain)\n\td.logger.Debug(\"sdk request 'console.GetHttpsServiceManager'\", slog.String(\"request.domain\", domain), slog.Any(\"response\", getHttpsServiceManagerResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'console.GetHttpsServiceManager': %w\", err)\n\t}\n\n\t// 判断域名是否已启用 HTTPS\n\t// 如果已启用，迁移域名证书；否则，设置新证书\n\t_, lastCertIndex, _ := lo.FindIndexOf(getHttpsServiceManagerResp.Data.Domains, func(item upyunsdk.HttpsServiceManagerDomain) bool {\n\t\treturn item.Https\n\t})\n\tif lastCertIndex == -1 {\n\t\tupdateHttpsCertificateManagerReq := &upyunsdk.UpdateHttpsCertificateManagerRequest{\n\t\t\tCertificateId: cloudCertId,\n\t\t\tDomain:        domain,\n\t\t\tHttps:         true,\n\t\t\tForceHttps:    true,\n\t\t}\n\t\tupdateHttpsCertificateManagerResp, err := d.sdkClient.UpdateHttpsCertificateManagerWithContext(ctx, updateHttpsCertificateManagerReq)\n\t\td.logger.Debug(\"sdk request 'console.EnableDomainHttps'\", slog.Any(\"request\", updateHttpsCertificateManagerReq), slog.Any(\"response\", updateHttpsCertificateManagerResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'console.UpdateHttpsCertificateManager': %w\", err)\n\t\t}\n\t} else if getHttpsServiceManagerResp.Data.Domains[lastCertIndex].CertificateId != cloudCertId {\n\t\tmigrateHttpsDomainReq := &upyunsdk.MigrateHttpsDomainRequest{\n\t\t\tCertificateId: cloudCertId,\n\t\t\tDomain:        domain,\n\t\t}\n\t\tmigrateHttpsDomainResp, err := d.sdkClient.MigrateHttpsDomainWithContext(ctx, migrateHttpsDomainReq)\n\t\td.logger.Debug(\"sdk request 'console.MigrateHttpsDomain'\", slog.Any(\"request\", migrateHttpsDomainReq), slog.Any(\"response\", migrateHttpsDomainResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'console.MigrateHttpsDomain': %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(username, password string) (*upyunsdk.Client, error) {\n\treturn upyunsdk.NewClient(username, password)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/upyun-cdn/upyun_cdn_test.go",
    "content": "package upyuncdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/upyun-cdn\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfUsername      string\n\tfPassword      string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"UPYUNCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fUsername, argsPrefix+\"USERNAME\", \"\", \"\")\n\tflag.StringVar(&fPassword, argsPrefix+\"PASSWORD\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./upyun_cdn_test.go -args \\\n\t--UPYUNCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--UPYUNCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--UPYUNCDN_USERNAME=\"your-username\" \\\n\t--UPYUNCDN_PASSWORD=\"your-password\" \\\n\t--UPYUNCDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"USERNAME: %v\", fUsername),\n\t\t\tfmt.Sprintf(\"PASSWORD: %v\", fPassword),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tUsername: fUsername,\n\t\t\tPassword: fPassword,\n\t\t\tDomain:   fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/upyun-file/upyun_file.go",
    "content": "package upyunfile\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/upyun-ssl\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\tupyunsdk \"github.com/certimate-go/certimate/pkg/sdk3rd/upyun/console\"\n)\n\ntype DeployerConfig struct {\n\t// 又拍云账号用户名。\n\tUsername string `json:\"username\"`\n\t// 又拍云账号密码。\n\tPassword string `json:\"password\"`\n\t// 存储桶名。暂时无用。\n\tBucket string `json:\"bucket\"`\n\t// 自定义域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *upyunsdk.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.Username, config.Password)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tUsername: config.Username,\n\t\tPassword: config.Password,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取域名证书配置\n\tgetHttpsServiceManagerResp, err := d.sdkClient.GetHttpsServiceManagerWithContext(ctx, d.config.Domain)\n\td.logger.Debug(\"sdk request 'console.GetHttpsServiceManager'\", slog.String(\"request.domain\", d.config.Domain), slog.Any(\"response\", getHttpsServiceManagerResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'console.GetHttpsServiceManager': %w\", err)\n\t}\n\n\t// 判断域名是否已启用 HTTPS\n\t// 如果已启用，迁移域名证书；否则，设置新证书\n\t_, lastCertIndex, _ := lo.FindIndexOf(getHttpsServiceManagerResp.Data.Domains, func(item upyunsdk.HttpsServiceManagerDomain) bool {\n\t\treturn item.Https\n\t})\n\tif lastCertIndex == -1 {\n\t\tupdateHttpsCertificateManagerReq := &upyunsdk.UpdateHttpsCertificateManagerRequest{\n\t\t\tCertificateId: upres.CertId,\n\t\t\tDomain:        d.config.Domain,\n\t\t\tHttps:         true,\n\t\t\tForceHttps:    true,\n\t\t}\n\t\tupdateHttpsCertificateManagerResp, err := d.sdkClient.UpdateHttpsCertificateManagerWithContext(ctx, updateHttpsCertificateManagerReq)\n\t\td.logger.Debug(\"sdk request 'console.EnableDomainHttps'\", slog.Any(\"request\", updateHttpsCertificateManagerReq), slog.Any(\"response\", updateHttpsCertificateManagerResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'console.UpdateHttpsCertificateManager': %w\", err)\n\t\t}\n\t} else if getHttpsServiceManagerResp.Data.Domains[lastCertIndex].CertificateId != upres.CertId {\n\t\tmigrateHttpsDomainReq := &upyunsdk.MigrateHttpsDomainRequest{\n\t\t\tCertificateId: upres.CertId,\n\t\t\tDomain:        d.config.Domain,\n\t\t}\n\t\tmigrateHttpsDomainResp, err := d.sdkClient.MigrateHttpsDomainWithContext(ctx, migrateHttpsDomainReq)\n\t\td.logger.Debug(\"sdk request 'console.MigrateHttpsDomain'\", slog.Any(\"request\", migrateHttpsDomainReq), slog.Any(\"response\", migrateHttpsDomainResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'console.MigrateHttpsDomain': %w\", err)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(username, password string) (*upyunsdk.Client, error) {\n\treturn upyunsdk.NewClient(username, password)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/upyun-file/upyun_file_test.go",
    "content": "package upyunfile_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/upyun-file\"\n)\n\nvar (\n\tfInputCertPath string\n\tfInputKeyPath  string\n\tfUsername      string\n\tfPassword      string\n\tfBucket        string\n\tfDomain        string\n)\n\nfunc init() {\n\targsPrefix := \"UPYUNFILE_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fUsername, argsPrefix+\"USERNAME\", \"\", \"\")\n\tflag.StringVar(&fPassword, argsPrefix+\"PASSWORD\", \"\", \"\")\n\tflag.StringVar(&fBucket, argsPrefix+\"BUCKET\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./upyun_file_test.go -args \\\n\t--UPYUNFILE_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--UPYUNFILE_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--UPYUNFILE_USERNAME=\"your-username\" \\\n\t--UPYUNFILE_PASSWORD=\"your-password\" \\\n\t--UPYUNFILE_BUCKET=\"your-bucket\" \\\n\t--UPYUNFILE_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"USERNAME: %v\", fUsername),\n\t\t\tfmt.Sprintf(\"PASSWORD: %v\", fPassword),\n\t\t\tfmt.Sprintf(\"BUCKET: %v\", fBucket),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tUsername: fUsername,\n\t\t\tPassword: fPassword,\n\t\t\tBucket:   fBucket,\n\t\t\tDomain:   fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-alb/consts.go",
    "content": "package volcenginealb\n\nconst (\n\t// 资源类型：部署到指定负载均衡器。\n\tRESOURCE_TYPE_LOADBALANCER = \"loadbalancer\"\n\t// 资源类型：部署到指定监听器。\n\tRESOURCE_TYPE_LISTENER = \"listener\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-alb/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"github.com/volcengine/volcengine-go-sdk/service/alb\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/client\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/client/metadata\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/corehandlers\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/request\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/signer/volc\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/volcenginequery\"\n)\n\n// This is a partial copy of https://github.com/volcengine/volcengine-go-sdk/blob/master/service/alb/service_alb.go\n// to lightweight the vendor packages in the built binary.\ntype AlbClient struct {\n\t*client.Client\n}\n\nfunc NewAlbClient(p client.ConfigProvider, cfgs ...*volcengine.Config) *AlbClient {\n\tc := p.ClientConfig(alb.EndpointsID, cfgs...)\n\treturn newAlbClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, c.SigningName)\n}\n\nfunc newAlbClient(cfg volcengine.Config, handlers request.Handlers, endpoint, signingRegion, signingName string) *AlbClient {\n\tsvc := &AlbClient{\n\t\tClient: client.New(\n\t\t\tcfg,\n\t\t\tmetadata.ClientInfo{\n\t\t\t\tServiceName:   alb.ServiceName,\n\t\t\t\tServiceID:     alb.ServiceID,\n\t\t\t\tSigningName:   signingName,\n\t\t\t\tSigningRegion: signingRegion,\n\t\t\t\tEndpoint:      endpoint,\n\t\t\t\tAPIVersion:    \"2020-04-01\",\n\t\t\t},\n\t\t\thandlers,\n\t\t),\n\t}\n\n\tsvc.Handlers.Build.PushBackNamed(corehandlers.SDKVersionUserAgentHandler)\n\tsvc.Handlers.Build.PushBackNamed(corehandlers.AddHostExecEnvUserAgentHandler)\n\tsvc.Handlers.Sign.PushBackNamed(volc.SignRequestHandler)\n\tsvc.Handlers.Build.PushBackNamed(volcenginequery.BuildHandler)\n\tsvc.Handlers.Unmarshal.PushBackNamed(volcenginequery.UnmarshalHandler)\n\tsvc.Handlers.UnmarshalMeta.PushBackNamed(volcenginequery.UnmarshalMetaHandler)\n\tsvc.Handlers.UnmarshalError.PushBackNamed(volcenginequery.UnmarshalErrorHandler)\n\n\treturn svc\n}\n\nfunc (c *AlbClient) newRequest(op *request.Operation, params, data interface{}) *request.Request {\n\treq := c.NewRequest(op, params, data)\n\n\treturn req\n}\n\nfunc (c *AlbClient) DescribeListenerAttributes(input *alb.DescribeListenerAttributesInput) (*alb.DescribeListenerAttributesOutput, error) {\n\treq, out := c.DescribeListenerAttributesRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *AlbClient) DescribeListenerAttributesRequest(input *alb.DescribeListenerAttributesInput) (req *request.Request, output *alb.DescribeListenerAttributesOutput) {\n\top := &request.Operation{\n\t\tName:       \"DescribeListenerAttributes\",\n\t\tHTTPMethod: \"GET\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &alb.DescribeListenerAttributesInput{}\n\t}\n\n\toutput = &alb.DescribeListenerAttributesOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treturn\n}\n\nfunc (c *AlbClient) DescribeListeners(input *alb.DescribeListenersInput) (*alb.DescribeListenersOutput, error) {\n\treq, out := c.DescribeListenersRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *AlbClient) DescribeListenersRequest(input *alb.DescribeListenersInput) (req *request.Request, output *alb.DescribeListenersOutput) {\n\top := &request.Operation{\n\t\tName:       \"DescribeListeners\",\n\t\tHTTPMethod: \"GET\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &alb.DescribeListenersInput{}\n\t}\n\n\toutput = &alb.DescribeListenersOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treturn\n}\n\nfunc (c *AlbClient) DescribeLoadBalancerAttributes(input *alb.DescribeLoadBalancerAttributesInput) (*alb.DescribeLoadBalancerAttributesOutput, error) {\n\treq, out := c.DescribeLoadBalancerAttributesRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *AlbClient) DescribeLoadBalancerAttributesRequest(input *alb.DescribeLoadBalancerAttributesInput) (req *request.Request, output *alb.DescribeLoadBalancerAttributesOutput) {\n\top := &request.Operation{\n\t\tName:       \"DescribeLoadBalancerAttributes\",\n\t\tHTTPMethod: \"GET\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &alb.DescribeLoadBalancerAttributesInput{}\n\t}\n\n\toutput = &alb.DescribeLoadBalancerAttributesOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treturn\n}\n\nfunc (c *AlbClient) ModifyListenerAttributes(input *alb.ModifyListenerAttributesInput) (*alb.ModifyListenerAttributesOutput, error) {\n\treq, out := c.ModifyListenerAttributesRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *AlbClient) ModifyListenerAttributesRequest(input *alb.ModifyListenerAttributesInput) (req *request.Request, output *alb.ModifyListenerAttributesOutput) {\n\top := &request.Operation{\n\t\tName:       \"ModifyListenerAttributes\",\n\t\tHTTPMethod: \"GET\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &alb.ModifyListenerAttributesInput{}\n\t}\n\n\toutput = &alb.ModifyListenerAttributesOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go",
    "content": "package volcenginealb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/samber/lo\"\n\tvealb \"github.com/volcengine/volcengine-go-sdk/service/alb\"\n\tve \"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\tvesession \"github.com/volcengine/volcengine-go-sdk/volcengine/session\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-alb/internal\"\n)\n\ntype DeployerConfig struct {\n\t// 火山引擎 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 火山引擎 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 火山引擎地域。\n\tRegion string `json:\"region\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 负载均衡实例 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。\n\tLoadbalancerId string `json:\"loadbalancerId,omitempty\"`\n\t// 负载均衡监听器 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。\n\tListenerId string `json:\"listenerId,omitempty\"`\n\t// SNI 域名（支持泛域名）。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。\n\tDomain string `json:\"domain,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.AlbClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tRegion:          config.Region,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_LOADBALANCER:\n\t\tif err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_LISTENER:\n\t\tif err := d.deployToListener(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\n\t// 查询 ALB 实例的详细信息\n\t// REF: https://www.volcengine.com/docs/6767/113596\n\tdescribeLoadBalancerAttributesReq := &vealb.DescribeLoadBalancerAttributesInput{\n\t\tLoadBalancerId: ve.String(d.config.LoadbalancerId),\n\t}\n\tdescribeLoadBalancerAttributesResp, err := d.sdkClient.DescribeLoadBalancerAttributes(describeLoadBalancerAttributesReq)\n\td.logger.Debug(\"sdk request 'alb.DescribeLoadBalancerAttributes'\", slog.Any(\"request\", describeLoadBalancerAttributesReq), slog.Any(\"response\", describeLoadBalancerAttributesResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'alb.DescribeLoadBalancerAttributes': %w\", err)\n\t}\n\n\t// 查询 HTTPS 监听器列表\n\t// REF: https://www.volcengine.com/docs/6767/113684\n\tlistenerIds := make([]string, 0)\n\tdescribeListenersPageSize := 100\n\tdescribeListenersPageNumber := 1\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeListenersReq := &vealb.DescribeListenersInput{\n\t\t\tLoadBalancerId: ve.String(d.config.LoadbalancerId),\n\t\t\tProtocol:       ve.String(\"HTTPS\"),\n\t\t\tPageNumber:     ve.Int64(int64(describeListenersPageNumber)),\n\t\t\tPageSize:       ve.Int64(int64(describeListenersPageSize)),\n\t\t}\n\t\tdescribeListenersResp, err := d.sdkClient.DescribeListeners(describeListenersReq)\n\t\td.logger.Debug(\"sdk request 'alb.DescribeListeners'\", slog.Any(\"request\", describeListenersReq), slog.Any(\"response\", describeListenersResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'alb.DescribeListeners': %w\", err)\n\t\t}\n\n\t\tfor _, listener := range describeListenersResp.Listeners {\n\t\t\tlistenerIds = append(listenerIds, *listener.ListenerId)\n\t\t}\n\n\t\tif len(describeListenersResp.Listeners) < describeListenersPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeListenersPageNumber++\n\t}\n\n\t// 遍历更新监听证书\n\tif len(listenerIds) == 0 {\n\t\td.logger.Info(\"no alb listeners to deploy\")\n\t} else {\n\t\td.logger.Info(\"found https listeners to deploy\", slog.Any(\"listenerIds\", listenerIds))\n\t\tvar errs []error\n\n\t\tfor _, listenerId := range listenerIds {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error {\n\tif d.config.ListenerId == \"\" {\n\t\treturn errors.New(\"config `listenerId` is required\")\n\t}\n\n\tif err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {\n\t// 查询指定监听器的详细信息\n\t// REF: https://www.volcengine.com/docs/6767/113686\n\tdescribeListenerAttributesReq := &vealb.DescribeListenerAttributesInput{\n\t\tListenerId: ve.String(cloudListenerId),\n\t}\n\tdescribeListenerAttributesResp, err := d.sdkClient.DescribeListenerAttributes(describeListenerAttributesReq)\n\td.logger.Debug(\"sdk request 'alb.DescribeListenerAttributes'\", slog.Any(\"request\", describeListenerAttributesReq), slog.Any(\"response\", describeListenerAttributesResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'alb.DescribeListenerAttributes': %w\", err)\n\t}\n\n\tif d.config.Domain == \"\" {\n\t\t// 未指定 SNI，只需部署到监听器\n\n\t\t// 修改指定监听器\n\t\t// REF: https://www.volcengine.com/docs/6767/113683\n\t\tmodifyListenerAttributesReq := &vealb.ModifyListenerAttributesInput{\n\t\t\tListenerId:              ve.String(cloudListenerId),\n\t\t\tCertificateSource:       ve.String(\"cert_center\"),\n\t\t\tCertCenterCertificateId: ve.String(cloudCertId),\n\t\t}\n\t\tmodifyListenerAttributesResp, err := d.sdkClient.ModifyListenerAttributes(modifyListenerAttributesReq)\n\t\td.logger.Debug(\"sdk request 'alb.ModifyListenerAttributes'\", slog.Any(\"request\", modifyListenerAttributesReq), slog.Any(\"response\", modifyListenerAttributesResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'alb.ModifyListenerAttributes': %w\", err)\n\t\t}\n\t} else {\n\t\t// 指定 SNI，需部署到扩展域名\n\n\t\t// 修改指定监听器\n\t\t// REF: https://www.volcengine.com/docs/6767/113683\n\t\tmodifyListenerAttributesReq := &vealb.ModifyListenerAttributesInput{\n\t\t\tListenerId: ve.String(cloudListenerId),\n\t\t\tDomainExtensions: lo.Map(\n\t\t\t\tlo.Filter(\n\t\t\t\t\tdescribeListenerAttributesResp.DomainExtensions,\n\t\t\t\t\tfunc(domain *vealb.DomainExtensionForDescribeListenerAttributesOutput, _ int) bool {\n\t\t\t\t\t\treturn *domain.Domain == d.config.Domain\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t\tfunc(domain *vealb.DomainExtensionForDescribeListenerAttributesOutput, _ int) *vealb.DomainExtensionForModifyListenerAttributesInput {\n\t\t\t\t\treturn &vealb.DomainExtensionForModifyListenerAttributesInput{\n\t\t\t\t\t\tDomainExtensionId:       domain.DomainExtensionId,\n\t\t\t\t\t\tDomain:                  domain.Domain,\n\t\t\t\t\t\tCertificateSource:       ve.String(\"cert_center\"),\n\t\t\t\t\t\tCertCenterCertificateId: ve.String(cloudCertId),\n\t\t\t\t\t\tAction:                  ve.String(\"modify\"),\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t}\n\t\tmodifyListenerAttributesResp, err := d.sdkClient.ModifyListenerAttributes(modifyListenerAttributesReq)\n\t\td.logger.Debug(\"sdk request 'alb.ModifyListenerAttributes'\", slog.Any(\"request\", modifyListenerAttributesReq), slog.Any(\"response\", modifyListenerAttributesResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'alb.ModifyListenerAttributes': %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.AlbClient, error) {\n\tconfig := ve.NewConfig().\n\t\tWithAkSk(accessKeyId, accessKeySecret).\n\t\tWithRegion(region)\n\n\tsession, err := vesession.NewSession(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := internal.NewAlbClient(session)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-alb/volcengine_alb_test.go",
    "content": "package volcenginealb_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-alb\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfListenerId      string\n)\n\nfunc init() {\n\targsPrefix := \"VOLCENGINEALB_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fListenerId, argsPrefix+\"LISTENERID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./volcengine_alb_test.go -args \\\n\t--VOLCENGINEALB_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--VOLCENGINEALB_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--VOLCENGINEALB_ACCESSKEYID=\"your-access-key-id\" \\\n\t--VOLCENGINEALB_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--VOLCENGINEALB_REGION=\"cn-beijing\" \\\n\t--VOLCENGINEALB_LISTENERID=\"your-listener-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LISTENERID: %v\", fListenerId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegion:          fRegion,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LISTENER,\n\t\t\tListenerId:      fListenerId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-cdn/consts.go",
    "content": "package volcenginecdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-cdn/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"github.com/volcengine/volcengine-go-sdk/service/cdn\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/client\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/client/metadata\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/corehandlers\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/request\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/signer/volc\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/volcenginequery\"\n)\n\n// This is a partial copy of https://github.com/volcengine/volcengine-go-sdk/blob/master/service/cdn/service_cdn.go\n// to lightweight the vendor packages in the built binary.\ntype CdnClient struct {\n\t*client.Client\n}\n\nfunc NewCdnClient(p client.ConfigProvider, cfgs ...*volcengine.Config) *CdnClient {\n\tc := p.ClientConfig(cdn.EndpointsID, cfgs...)\n\treturn newCdnClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, c.SigningName)\n}\n\nfunc newCdnClient(cfg volcengine.Config, handlers request.Handlers, endpoint, signingRegion, signingName string) *CdnClient {\n\tsvc := &CdnClient{\n\t\tClient: client.New(\n\t\t\tcfg,\n\t\t\tmetadata.ClientInfo{\n\t\t\t\tServiceName:   cdn.ServiceName,\n\t\t\t\tServiceID:     cdn.ServiceID,\n\t\t\t\tSigningName:   signingName,\n\t\t\t\tSigningRegion: signingRegion,\n\t\t\t\tEndpoint:      endpoint,\n\t\t\t\tAPIVersion:    \"2021-03-01\",\n\t\t\t},\n\t\t\thandlers,\n\t\t),\n\t}\n\n\tsvc.Handlers.Build.PushBackNamed(corehandlers.SDKVersionUserAgentHandler)\n\tsvc.Handlers.Build.PushBackNamed(corehandlers.AddHostExecEnvUserAgentHandler)\n\tsvc.Handlers.Sign.PushBackNamed(volc.SignRequestHandler)\n\tsvc.Handlers.Build.PushBackNamed(volcenginequery.BuildHandler)\n\tsvc.Handlers.Unmarshal.PushBackNamed(volcenginequery.UnmarshalHandler)\n\tsvc.Handlers.UnmarshalMeta.PushBackNamed(volcenginequery.UnmarshalMetaHandler)\n\tsvc.Handlers.UnmarshalError.PushBackNamed(volcenginequery.UnmarshalErrorHandler)\n\n\treturn svc\n}\n\nfunc (c *CdnClient) newRequest(op *request.Operation, params, data interface{}) *request.Request {\n\treq := c.NewRequest(op, params, data)\n\n\treturn req\n}\n\nfunc (c *CdnClient) BatchDeployCert(input *cdn.BatchDeployCertInput) (*cdn.BatchDeployCertOutput, error) {\n\treq, out := c.BatchDeployCertRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *CdnClient) BatchDeployCertRequest(input *cdn.BatchDeployCertInput) (req *request.Request, output *cdn.BatchDeployCertOutput) {\n\top := &request.Operation{\n\t\tName:       \"BatchDeployCert\",\n\t\tHTTPMethod: \"POST\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &cdn.BatchDeployCertInput{}\n\t}\n\n\toutput = &cdn.BatchDeployCertOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treq.HTTPRequest.Header.Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\n\treturn\n}\n\nfunc (c *CdnClient) DescribeCertConfig(input *cdn.DescribeCertConfigInput) (*cdn.DescribeCertConfigOutput, error) {\n\treq, out := c.DescribeCertConfigRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *CdnClient) DescribeCertConfigRequest(input *cdn.DescribeCertConfigInput) (req *request.Request, output *cdn.DescribeCertConfigOutput) {\n\top := &request.Operation{\n\t\tName:       \"DescribeCertConfig\",\n\t\tHTTPMethod: \"POST\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &cdn.DescribeCertConfigInput{}\n\t}\n\n\toutput = &cdn.DescribeCertConfigOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treq.HTTPRequest.Header.Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\n\treturn\n}\n\nfunc (c *CdnClient) ListCdnDomains(input *cdn.ListCdnDomainsInput) (*cdn.ListCdnDomainsOutput, error) {\n\treq, out := c.ListCdnDomainsRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *CdnClient) ListCdnDomainsRequest(input *cdn.ListCdnDomainsInput) (req *request.Request, output *cdn.ListCdnDomainsOutput) {\n\top := &request.Operation{\n\t\tName:       \"ListCdnDomains\",\n\t\tHTTPMethod: \"POST\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &cdn.ListCdnDomainsInput{}\n\t}\n\n\toutput = &cdn.ListCdnDomainsOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treq.HTTPRequest.Header.Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-cdn/volcengine_cdn.go",
    "content": "package volcenginecdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\tvecdn \"github.com/volcengine/volcengine-go-sdk/service/cdn\"\n\tve \"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\tvesession \"github.com/volcengine/volcengine-go-sdk/volcengine/session\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-cdn\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-cdn/internal\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 火山引擎 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 火山引擎 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.CdnClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tdomains := make([]string, 0)\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = []string{d.config.Domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getMatchedDomainsByWildcard(ctx, d.config.Domain)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = domainCandidates\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tdomainCandidates, err := d.getMatchedDomainsByCertId(ctx, upres.CertId)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = domainCandidates\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历绑定证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no cdn domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found cdn domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getMatchedDomainsByWildcard(ctx context.Context, wildcardDomain string) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询加速域名列表，获取匹配的域名\n\t// REF: https://www.volcengine.com/docs/6454/75269\n\tlistCdnDomainsPageNum := 1\n\tlistCdnDomainsPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistCdnDomainsReq := &vecdn.ListCdnDomainsInput{\n\t\t\tDomain:   ve.String(strings.TrimPrefix(wildcardDomain, \"*.\")),\n\t\t\tStatus:   ve.String(\"online\"),\n\t\t\tPageNum:  ve.Int64(int64(listCdnDomainsPageNum)),\n\t\t\tPageSize: ve.Int64(int64(listCdnDomainsPageSize)),\n\t\t}\n\t\tlistCdnDomainsResp, err := d.sdkClient.ListCdnDomains(listCdnDomainsReq)\n\t\td.logger.Debug(\"sdk request 'cdn.ListCdnDomains'\", slog.Any(\"request\", listCdnDomainsReq), slog.Any(\"response\", listCdnDomainsResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.ListCdnDomains': %w\", err)\n\t\t}\n\n\t\tfor _, domainItem := range listCdnDomainsResp.Data {\n\t\t\tif xcerthostname.IsMatch(wildcardDomain, ve.StringValue(domainItem.Domain)) {\n\t\t\t\tdomains = append(domains, ve.StringValue(domainItem.Domain))\n\t\t\t}\n\t\t}\n\n\t\tif len(listCdnDomainsResp.Data) < listCdnDomainsPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tlistCdnDomainsPageSize++\n\t}\n\n\tif len(domains) == 0 {\n\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) getMatchedDomainsByCertId(ctx context.Context, cloudCertId string) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 获取指定证书可关联的域名\n\t// REF: https://www.volcengine.com/docs/6454/125711\n\tdescribeCertConfigReq := &vecdn.DescribeCertConfigInput{\n\t\tCertId: ve.String(cloudCertId),\n\t}\n\tdescribeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq)\n\td.logger.Debug(\"sdk request 'cdn.DescribeCertConfig'\", slog.Any(\"request\", describeCertConfigReq), slog.Any(\"response\", describeCertConfigResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.DescribeCertConfig': %w\", err)\n\t}\n\n\tif describeCertConfigResp.CertNotConfig != nil {\n\t\tfor i := range describeCertConfigResp.CertNotConfig {\n\t\t\tdomains = append(domains, ve.StringValue(describeCertConfigResp.CertNotConfig[i].Domain))\n\t\t}\n\t}\n\n\tif describeCertConfigResp.OtherCertConfig != nil {\n\t\tfor i := range describeCertConfigResp.OtherCertConfig {\n\t\t\tdomains = append(domains, ve.StringValue(describeCertConfigResp.OtherCertConfig[i].Domain))\n\t\t}\n\t}\n\n\tif len(domains) == 0 {\n\t\tif len(describeCertConfigResp.SpecifiedCertConfig) == 0 {\n\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t}\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error {\n\t// 关联证书与加速域名\n\t// REF: https://www.volcengine.com/docs/6454/125712\n\tbatchDeployCertReq := &vecdn.BatchDeployCertInput{\n\t\tDomain: ve.String(domain),\n\t\tCertId: ve.String(cloudCertId),\n\t}\n\tbatchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)\n\td.logger.Debug(\"sdk request 'cdn.BatchDeployCert'\", slog.Any(\"request\", batchDeployCertReq), slog.Any(\"response\", batchDeployCertResp))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret string) (*internal.CdnClient, error) {\n\tconfig := ve.NewConfig().\n\t\tWithAkSk(accessKeyId, accessKeySecret).\n\t\tWithRegion(\"cn-north-1\")\n\n\tsession, err := vesession.NewSession(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := internal.NewCdnClient(session)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-cdn/volcengine_cdn_test.go",
    "content": "package volcenginecdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-cdn\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"VOLCENGINECDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./volcengine_cdn_test.go -args \\\n\t--VOLCENGINECDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--VOLCENGINECDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--VOLCENGINECDN_ACCESSKEYID=\"your-access-key-id\" \\\n\t--VOLCENGINECDN_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--VOLCENGINECDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tAccessKeySecret:    fAccessKeySecret,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-certcenter/volcengine_certcenter.go",
    "content": "package volcenginecertcenter\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// 火山引擎 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 火山引擎 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 火山引擎地域。\n\tRegion string `json:\"region\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tRegion:          config.Region,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-clb/consts.go",
    "content": "package volcengineclb\n\nconst (\n\t// 资源类型：部署到指定负载均衡器。\n\tRESOURCE_TYPE_LOADBALANCER = \"loadbalancer\"\n\t// 资源类型：部署到指定监听器。\n\tRESOURCE_TYPE_LISTENER = \"listener\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-clb/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"github.com/volcengine/volcengine-go-sdk/service/clb\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/client\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/client/metadata\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/corehandlers\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/request\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/signer/volc\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/volcenginequery\"\n)\n\n// This is a partial copy of https://github.com/volcengine/volcengine-go-sdk/blob/master/service/clb/service_clb.go\n// to lightweight the vendor packages in the built binary.\ntype ClbClient struct {\n\t*client.Client\n}\n\nfunc NewClbClient(p client.ConfigProvider, cfgs ...*volcengine.Config) *ClbClient {\n\tc := p.ClientConfig(clb.EndpointsID, cfgs...)\n\treturn newClbClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, c.SigningName)\n}\n\nfunc newClbClient(cfg volcengine.Config, handlers request.Handlers, endpoint, signingRegion, signingName string) *ClbClient {\n\tsvc := &ClbClient{\n\t\tClient: client.New(\n\t\t\tcfg,\n\t\t\tmetadata.ClientInfo{\n\t\t\t\tServiceName:   clb.ServiceName,\n\t\t\t\tServiceID:     clb.ServiceID,\n\t\t\t\tSigningName:   signingName,\n\t\t\t\tSigningRegion: signingRegion,\n\t\t\t\tEndpoint:      endpoint,\n\t\t\t\tAPIVersion:    \"2020-04-01\",\n\t\t\t},\n\t\t\thandlers,\n\t\t),\n\t}\n\n\tsvc.Handlers.Build.PushBackNamed(corehandlers.SDKVersionUserAgentHandler)\n\tsvc.Handlers.Build.PushBackNamed(corehandlers.AddHostExecEnvUserAgentHandler)\n\tsvc.Handlers.Sign.PushBackNamed(volc.SignRequestHandler)\n\tsvc.Handlers.Build.PushBackNamed(volcenginequery.BuildHandler)\n\tsvc.Handlers.Unmarshal.PushBackNamed(volcenginequery.UnmarshalHandler)\n\tsvc.Handlers.UnmarshalMeta.PushBackNamed(volcenginequery.UnmarshalMetaHandler)\n\tsvc.Handlers.UnmarshalError.PushBackNamed(volcenginequery.UnmarshalErrorHandler)\n\n\treturn svc\n}\n\nfunc (c *ClbClient) newRequest(op *request.Operation, params, data interface{}) *request.Request {\n\treq := c.NewRequest(op, params, data)\n\n\treturn req\n}\n\nfunc (c *ClbClient) DescribeListeners(input *clb.DescribeListenersInput) (*clb.DescribeListenersOutput, error) {\n\treq, out := c.DescribeListenersRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *ClbClient) DescribeListenersRequest(input *clb.DescribeListenersInput) (req *request.Request, output *clb.DescribeListenersOutput) {\n\top := &request.Operation{\n\t\tName:       \"DescribeListeners\",\n\t\tHTTPMethod: \"GET\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &clb.DescribeListenersInput{}\n\t}\n\n\toutput = &clb.DescribeListenersOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treturn\n}\n\nfunc (c *ClbClient) DescribeLoadBalancerAttributes(input *clb.DescribeLoadBalancerAttributesInput) (*clb.DescribeLoadBalancerAttributesOutput, error) {\n\treq, out := c.DescribeLoadBalancerAttributesRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *ClbClient) DescribeLoadBalancerAttributesRequest(input *clb.DescribeLoadBalancerAttributesInput) (req *request.Request, output *clb.DescribeLoadBalancerAttributesOutput) {\n\top := &request.Operation{\n\t\tName:       \"DescribeLoadBalancerAttributes\",\n\t\tHTTPMethod: \"GET\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &clb.DescribeLoadBalancerAttributesInput{}\n\t}\n\n\toutput = &clb.DescribeLoadBalancerAttributesOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treturn\n}\n\nfunc (c *ClbClient) ModifyListenerAttributes(input *clb.ModifyListenerAttributesInput) (*clb.ModifyListenerAttributesOutput, error) {\n\treq, out := c.ModifyListenerAttributesRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *ClbClient) ModifyListenerAttributesRequest(input *clb.ModifyListenerAttributesInput) (req *request.Request, output *clb.ModifyListenerAttributesOutput) {\n\top := &request.Operation{\n\t\tName:       \"ModifyListenerAttributes\",\n\t\tHTTPMethod: \"GET\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &clb.ModifyListenerAttributesInput{}\n\t}\n\n\toutput = &clb.ModifyListenerAttributesOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go",
    "content": "package volcengineclb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\tveclb \"github.com/volcengine/volcengine-go-sdk/service/clb\"\n\tve \"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\tvesession \"github.com/volcengine/volcengine-go-sdk/volcengine/session\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-clb/internal\"\n)\n\ntype DeployerConfig struct {\n\t// 火山引擎 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 火山引擎 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 火山引擎地域。\n\tRegion string `json:\"region\"`\n\t// 部署资源类型。\n\tResourceType string `json:\"resourceType\"`\n\t// 负载均衡实例 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。\n\tLoadbalancerId string `json:\"loadbalancerId,omitempty\"`\n\t// 负载均衡监听器 ID。\n\t// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。\n\tListenerId string `json:\"listenerId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.ClbClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tRegion:          config.Region,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 根据部署资源类型决定部署方式\n\tswitch d.config.ResourceType {\n\tcase RESOURCE_TYPE_LOADBALANCER:\n\t\tif err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tcase RESOURCE_TYPE_LISTENER:\n\t\tif err := d.deployToListener(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported resource type '%s'\", d.config.ResourceType)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {\n\tif d.config.LoadbalancerId == \"\" {\n\t\treturn errors.New(\"config `loadbalancerId` is required\")\n\t}\n\n\t// 查看指定负载均衡实例的详情\n\t// REF: https://www.volcengine.com/docs/6406/71773\n\tdescribeLoadBalancerAttributesReq := &veclb.DescribeLoadBalancerAttributesInput{\n\t\tLoadBalancerId: ve.String(d.config.LoadbalancerId),\n\t}\n\tdescribeLoadBalancerAttributesResp, err := d.sdkClient.DescribeLoadBalancerAttributes(describeLoadBalancerAttributesReq)\n\td.logger.Debug(\"sdk request 'clb.DescribeLoadBalancerAttributes'\", slog.Any(\"request\", describeLoadBalancerAttributesReq), slog.Any(\"response\", describeLoadBalancerAttributesResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'clb.DescribeLoadBalancerAttributes': %w\", err)\n\t}\n\n\t// 查询 HTTPS 监听器列表\n\t// REF: https://www.volcengine.com/docs/6406/71776\n\tlistenerIds := make([]string, 0)\n\tdescribeListenersPageSize := 100\n\tdescribeListenersPageNumber := 1\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tdescribeListenersReq := &veclb.DescribeListenersInput{\n\t\t\tLoadBalancerId: ve.String(d.config.LoadbalancerId),\n\t\t\tProtocol:       ve.String(\"HTTPS\"),\n\t\t\tPageNumber:     ve.Int64(int64(describeListenersPageNumber)),\n\t\t\tPageSize:       ve.Int64(int64(describeListenersPageSize)),\n\t\t}\n\t\tdescribeListenersResp, err := d.sdkClient.DescribeListeners(describeListenersReq)\n\t\td.logger.Debug(\"sdk request 'clb.DescribeListeners'\", slog.Any(\"request\", describeListenersReq), slog.Any(\"response\", describeListenersResp))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute sdk request 'clb.DescribeListeners': %w\", err)\n\t\t}\n\n\t\tfor _, listener := range describeListenersResp.Listeners {\n\t\t\tlistenerIds = append(listenerIds, *listener.ListenerId)\n\t\t}\n\n\t\tif len(describeListenersResp.Listeners) < describeListenersPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tdescribeListenersPageNumber++\n\t}\n\n\t// 遍历更新监听证书\n\tif len(listenerIds) == 0 {\n\t\td.logger.Info(\"no clb listeners to deploy\")\n\t} else {\n\t\td.logger.Info(\"found https listeners to deploy\", slog.Any(\"listenerIds\", listenerIds))\n\t\tvar errs []error\n\n\t\tfor _, listenerId := range listenerIds {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error {\n\tif d.config.ListenerId == \"\" {\n\t\treturn errors.New(\"config `listenerId` is required\")\n\t}\n\n\tif err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {\n\t// 修改指定监听器\n\t// REF: https://www.volcengine.com/docs/6406/71775\n\tmodifyListenerAttributesReq := &veclb.ModifyListenerAttributesInput{\n\t\tListenerId:              ve.String(cloudListenerId),\n\t\tCertificateSource:       ve.String(\"cert_center\"),\n\t\tCertCenterCertificateId: ve.String(cloudCertId),\n\t}\n\tmodifyListenerAttributesResp, err := d.sdkClient.ModifyListenerAttributes(modifyListenerAttributesReq)\n\td.logger.Debug(\"sdk request 'clb.ModifyListenerAttributes'\", slog.Any(\"request\", modifyListenerAttributesReq), slog.Any(\"response\", modifyListenerAttributesResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'clb.ModifyListenerAttributes': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.ClbClient, error) {\n\tconfig := ve.NewConfig().\n\t\tWithAkSk(accessKeyId, accessKeySecret).\n\t\tWithRegion(region)\n\n\tsession, err := vesession.NewSession(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := internal.NewClbClient(session)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-clb/volcengine_clb_test.go",
    "content": "package volcengineclb_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-clb\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfListenerId      string\n)\n\nfunc init() {\n\targsPrefix := \"VOLCENGINECLB_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fListenerId, argsPrefix+\"LISTENERID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./volcengine_clb_test.go -args \\\n\t--VOLCENGINECLB_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--VOLCENGINECLB_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--VOLCENGINECLB_ACCESSKEYID=\"your-access-key-id\" \\\n\t--VOLCENGINECLB_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--VOLCENGINECLB_REGION=\"cn-beijing\" \\\n\t--VOLCENGINECLB_LISTENERID=\"your-listener-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"LISTENERID: %v\", fListenerId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegion:          fRegion,\n\t\t\tResourceType:    provider.RESOURCE_TYPE_LISTENER,\n\t\t\tListenerId:      fListenerId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-dcdn/consts.go",
    "content": "package volcenginedcdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-dcdn/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"github.com/volcengine/volcengine-go-sdk/service/dcdn\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/client\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/client/metadata\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/corehandlers\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/request\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/signer/volc\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/volcenginequery\"\n)\n\n// This is a partial copy of https://github.com/volcengine/volcengine-go-sdk/blob/master/service/dcdn/service_dcdn.go\n// to lightweight the vendor packages in the built binary.\ntype DcdnClient struct {\n\t*client.Client\n}\n\nfunc NewDcdnClient(p client.ConfigProvider, cfgs ...*volcengine.Config) *DcdnClient {\n\tc := p.ClientConfig(dcdn.EndpointsID, cfgs...)\n\treturn newDcdnClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, c.SigningName)\n}\n\nfunc newDcdnClient(cfg volcengine.Config, handlers request.Handlers, endpoint, signingRegion, signingName string) *DcdnClient {\n\tsvc := &DcdnClient{\n\t\tClient: client.New(\n\t\t\tcfg,\n\t\t\tmetadata.ClientInfo{\n\t\t\t\tServiceName:   dcdn.ServiceName,\n\t\t\t\tServiceID:     dcdn.ServiceID,\n\t\t\t\tSigningName:   signingName,\n\t\t\t\tSigningRegion: signingRegion,\n\t\t\t\tEndpoint:      endpoint,\n\t\t\t\tAPIVersion:    \"2021-04-01\",\n\t\t\t},\n\t\t\thandlers,\n\t\t),\n\t}\n\n\tsvc.Handlers.Build.PushBackNamed(corehandlers.SDKVersionUserAgentHandler)\n\tsvc.Handlers.Build.PushBackNamed(corehandlers.AddHostExecEnvUserAgentHandler)\n\tsvc.Handlers.Sign.PushBackNamed(volc.SignRequestHandler)\n\tsvc.Handlers.Build.PushBackNamed(volcenginequery.BuildHandler)\n\tsvc.Handlers.Unmarshal.PushBackNamed(volcenginequery.UnmarshalHandler)\n\tsvc.Handlers.UnmarshalMeta.PushBackNamed(volcenginequery.UnmarshalMetaHandler)\n\tsvc.Handlers.UnmarshalError.PushBackNamed(volcenginequery.UnmarshalErrorHandler)\n\n\treturn svc\n}\n\nfunc (c *DcdnClient) newRequest(op *request.Operation, params, data interface{}) *request.Request {\n\treq := c.NewRequest(op, params, data)\n\n\treturn req\n}\n\nfunc (c *DcdnClient) CreateCertBind(input *dcdn.CreateCertBindInput) (*dcdn.CreateCertBindOutput, error) {\n\treq, out := c.CreateCertBindRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *DcdnClient) CreateCertBindRequest(input *dcdn.CreateCertBindInput) (req *request.Request, output *dcdn.CreateCertBindOutput) {\n\top := &request.Operation{\n\t\tName:       \"CreateCertBind\",\n\t\tHTTPMethod: \"POST\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &dcdn.CreateCertBindInput{}\n\t}\n\n\toutput = &dcdn.CreateCertBindOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treq.HTTPRequest.Header.Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\n\treturn\n}\n\nfunc (c *DcdnClient) ListDomainConfig(input *dcdn.ListDomainConfigInput) (*dcdn.ListDomainConfigOutput, error) {\n\treq, out := c.ListDomainConfigRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *DcdnClient) ListDomainConfigRequest(input *dcdn.ListDomainConfigInput) (req *request.Request, output *dcdn.ListDomainConfigOutput) {\n\top := &request.Operation{\n\t\tName:       \"ListDomainConfig\",\n\t\tHTTPMethod: \"POST\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &dcdn.ListDomainConfigInput{}\n\t}\n\n\toutput = &dcdn.ListDomainConfigOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treq.HTTPRequest.Header.Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-dcdn/volcengine_dcdn.go",
    "content": "package volcenginedcdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\tvedcdn \"github.com/volcengine/volcengine-go-sdk/service/dcdn\"\n\tve \"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\tvesession \"github.com/volcengine/volcengine-go-sdk/volcengine/session\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-dcdn/internal\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 火山引擎 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 火山引擎 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 火山引擎地域。\n\tRegion string `json:\"region\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.DcdnClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tRegion:          config.Region,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tdomains := make([]string, 0)\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\t// \"*.example.com\" → \".example.com\"，适配火山引擎 DCDN 要求的泛域名格式\n\t\t\tdomain := strings.TrimPrefix(d.config.Domain, \"*\")\n\t\t\tdomains = []string{domain}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain) ||\n\t\t\t\t\t\tstrings.TrimPrefix(d.config.Domain, \"*\") == strings.TrimPrefix(domain, \"*\")\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = []string{d.config.Domain}\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil ||\n\t\t\t\t\tstrings.TrimPrefix(d.config.Domain, \"*\") == strings.TrimPrefix(domain, \"*\")\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 批量绑定证书\n\t// REF: https://www.volcengine.com/docs/6559/1250189\n\tcreateCertBindReq := &vedcdn.CreateCertBindInput{\n\t\tCertSource:  ve.String(\"volc\"),\n\t\tCertId:      ve.String(upres.CertId),\n\t\tDomainNames: ve.StringSlice(domains),\n\t}\n\tcreateCertBindResp, err := d.sdkClient.CreateCertBind(createCertBindReq)\n\td.logger.Debug(\"sdk request 'dcdn.CreateCertBind'\", slog.Any(\"request\", createCertBindReq), slog.Any(\"response\", createCertBindResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'dcdn.CreateCertBind': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名配置列表\n\t// https://www.volcengine.com/docs/6559/1171745\n\tlistDomainConfigPageNumber := 1\n\tlistDomainConfigPageSize := 100\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistDomainConfigReq := &vedcdn.ListDomainConfigInput{\n\t\t\tPageNumber: ve.Int32(int32(listDomainConfigPageNumber)),\n\t\t\tPageSize:   ve.Int32(int32(listDomainConfigPageSize)),\n\t\t}\n\t\tlistDomainConfigResp, err := d.sdkClient.ListDomainConfig(listDomainConfigReq)\n\t\td.logger.Debug(\"sdk request 'dcdn.ListDomainConfig'\", slog.Any(\"request\", listDomainConfigReq), slog.Any(\"response\", listDomainConfigResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'dcdn.ListDomainConfig': %w\", err)\n\t\t}\n\n\t\tignoredStatuses := []string{\"Stop\"}\n\t\tfor _, domainItem := range listDomainConfigResp.DomainList {\n\t\t\tif lo.Contains(ignoredStatuses, *domainItem.Status) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdomains = append(domains, *domainItem.Domain)\n\t\t}\n\n\t\tif len(listDomainConfigResp.DomainList) < listDomainConfigPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tlistDomainConfigPageNumber++\n\t}\n\n\treturn domains, nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.DcdnClient, error) {\n\tif region == \"\" {\n\t\tregion = \"cn-beijing\" // DCDN 服务默认区域：北京\n\t}\n\n\tconfig := ve.NewConfig().\n\t\tWithAkSk(accessKeyId, accessKeySecret).\n\t\tWithRegion(region)\n\n\tsession, err := vesession.NewSession(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := internal.NewDcdnClient(session)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-dcdn/volcengine_dcdn_test.go",
    "content": "package volcenginedcdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-dcdn\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"VOLCENGINEDCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./volcengine_dcdn_test.go -args \\\n\t--VOLCENGINEDCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--VOLCENGINEDCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--VOLCENGINEDCDN_ACCESSKEYID=\"your-access-key-id\" \\\n\t--VOLCENGINEDCDN_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--VOLCENGINEDCDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tAccessKeySecret:    fAccessKeySecret,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-imagex/volcengine_imagex.go",
    "content": "package volcengineimagex\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\tvebase \"github.com/volcengine/volc-sdk-golang/base\"\n\tveimagex \"github.com/volcengine/volc-sdk-golang/service/imagex/v2\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// 火山引擎 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 火山引擎 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 火山引擎地域。\n\tRegion string `json:\"region\"`\n\t// 服务 ID。\n\tServiceId string `json:\"serviceId\"`\n\t// 自定义域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *veimagex.Imagex\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tRegion:          config.Region,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.ServiceId == \"\" {\n\t\treturn nil, errors.New(\"config `serviceId` is required\")\n\t}\n\tif d.config.Domain == \"\" {\n\t\treturn nil, errors.New(\"config `domain` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取域名配置\n\t// REF: https://www.volcengine.com/docs/508/9366\n\tgetDomainConfigReq := &veimagex.GetDomainConfigQuery{\n\t\tServiceID:  d.config.ServiceId,\n\t\tDomainName: d.config.Domain,\n\t}\n\tgetDomainConfigResp, err := d.sdkClient.GetDomainConfig(ctx, getDomainConfigReq)\n\td.logger.Debug(\"sdk request 'imagex.GetDomainConfig'\", slog.Any(\"request\", getDomainConfigReq), slog.Any(\"response\", getDomainConfigResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'imagex.GetDomainConfig': %w\", err)\n\t}\n\n\t// 更新 HTTPS 配置\n\t// REF: https://www.volcengine.com/docs/508/66012\n\tupdateHttpsReq := &veimagex.UpdateHTTPSReq{\n\t\tUpdateHTTPSQuery: &veimagex.UpdateHTTPSQuery{\n\t\t\tServiceID: d.config.ServiceId,\n\t\t},\n\t\tUpdateHTTPSBody: &veimagex.UpdateHTTPSBody{\n\t\t\tDomain: d.config.Domain,\n\t\t\tHTTPS: &veimagex.UpdateHTTPSBodyHTTPS{\n\t\t\t\tCertID:      upres.CertId,\n\t\t\t\tEnableHTTPS: true,\n\t\t\t},\n\t\t},\n\t}\n\tif getDomainConfigResp.Result != nil && getDomainConfigResp.Result.HTTPSConfig != nil {\n\t\tupdateHttpsReq.UpdateHTTPSBody.HTTPS.EnableHTTPS = getDomainConfigResp.Result.HTTPSConfig.EnableHTTPS\n\t\tupdateHttpsReq.UpdateHTTPSBody.HTTPS.EnableHTTP2 = getDomainConfigResp.Result.HTTPSConfig.EnableHTTP2\n\t\tupdateHttpsReq.UpdateHTTPSBody.HTTPS.EnableOcsp = getDomainConfigResp.Result.HTTPSConfig.EnableOcsp\n\t\tupdateHttpsReq.UpdateHTTPSBody.HTTPS.TLSVersions = getDomainConfigResp.Result.HTTPSConfig.TLSVersions\n\t\tupdateHttpsReq.UpdateHTTPSBody.HTTPS.EnableForceRedirect = getDomainConfigResp.Result.HTTPSConfig.EnableForceRedirect\n\t\tupdateHttpsReq.UpdateHTTPSBody.HTTPS.ForceRedirectType = getDomainConfigResp.Result.HTTPSConfig.ForceRedirectType\n\t\tupdateHttpsReq.UpdateHTTPSBody.HTTPS.ForceRedirectCode = getDomainConfigResp.Result.HTTPSConfig.ForceRedirectCode\n\t}\n\tupdateHttpsResp, err := d.sdkClient.UpdateHTTPS(ctx, updateHttpsReq)\n\td.logger.Debug(\"sdk request 'imagex.UpdateHttps'\", slog.Any(\"request\", updateHttpsReq), slog.Any(\"response\", updateHttpsResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'imagex.UpdateHttps': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*veimagex.Imagex, error) {\n\tvar instance *veimagex.Imagex\n\tif region == \"\" {\n\t\tinstance = veimagex.NewInstance()\n\t} else {\n\t\tinstance = veimagex.NewInstanceWithRegion(region)\n\t}\n\n\tinstance.SetCredential(vebase.Credentials{\n\t\tAccessKeyID:     accessKeyId,\n\t\tSecretAccessKey: accessKeySecret,\n\t})\n\n\treturn instance, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-imagex/volcengine_imagex_test.go",
    "content": "package volcengineimagex_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-imagex\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfServiceId       string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"VOLCENGINEIMAGEX_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fServiceId, argsPrefix+\"SERVICEID\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./volcengine_imagex_test.go -args \\\n\t--VOLCENGINEIMAGEX_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--VOLCENGINEIMAGEX_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--VOLCENGINEIMAGEX_ACCESSKEYID=\"your-access-key-id\" \\\n\t--VOLCENGINEIMAGEX_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--VOLCENGINEIMAGEX_REGION=\"cn-north-1\" \\\n\t--VOLCENGINEIMAGEX_SERVICEID=\"your-service-id\" \\\n\t--VOLCENGINEIMAGEX_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"SERVICEID: %v\", fServiceId),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegion:          fRegion,\n\t\t\tServiceId:       fServiceId,\n\t\t\tDomain:          fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-live/consts.go",
    "content": "package volcenginelive\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-live/volcengine_live.go",
    "content": "package volcenginelive\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\tvelive \"github.com/volcengine/volc-sdk-golang/service/live/v20230101\"\n\tve \"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-live\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 火山引擎 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 火山引擎 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 直播流域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *velive.Live\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient := velive.NewInstance()\n\tclient.SetAccessKey(config.AccessKeyId)\n\tclient.SetSecretKey(config.AccessKeySecret)\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的直播实例\n\tdomains := make([]string, 0)\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = append(domains, d.config.Domain)\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain)\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = append(domains, d.config.Domain)\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历绑定证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no live domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found live domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 查询域名列表\n\t// REF: https://www.volcengine.com/docs/6469/1126815\n\tlistDomainDetailPageNum := 1\n\tlistDomainDetailPageSize := 1000\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistDomainDetailReq := &velive.ListDomainDetailBody{\n\t\t\tDomainStatusList: ve.Int32Slice([]int32{0}),\n\t\t\tPageNum:          int32(listDomainDetailPageNum),\n\t\t\tPageSize:         int32(listDomainDetailPageSize),\n\t\t}\n\t\tlistDomainDetailResp, err := d.sdkClient.ListDomainDetail(ctx, listDomainDetailReq)\n\t\td.logger.Debug(\"sdk request 'live.ListDomainDetail'\", slog.Any(\"request\", listDomainDetailReq), slog.Any(\"response\", listDomainDetailResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'live.ListDomainDetail': %w\", err)\n\t\t}\n\n\t\tif listDomainDetailResp.Result == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, domainItem := range listDomainDetailResp.Result.DomainList {\n\t\t\tdomains = append(domains, domainItem.Domain)\n\t\t}\n\n\t\tif len(listDomainDetailResp.Result.DomainList) < listDomainDetailPageSize {\n\t\t\tbreak\n\t\t}\n\n\t\tlistDomainDetailPageNum++\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error {\n\t// 绑定证书\n\t// REF: https://www.volcengine.com/docs/6469/1126820\n\tbindCertReq := &velive.BindCertBody{\n\t\tChainID: cloudCertId,\n\t\tDomain:  domain,\n\t\tHTTPS:   ve.Bool(true),\n\t}\n\tbindCertResp, err := d.sdkClient.BindCert(ctx, bindCertReq)\n\td.logger.Debug(\"sdk request 'live.BindCert'\", slog.Any(\"request\", bindCertReq), slog.Any(\"response\", bindCertResp))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-live/volcengine_live_test.go",
    "content": "package volcenginelive_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-live\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"VOLCENGINELIVE_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./volcengine_live_test.go -args \\\n\t--VOLCENGINELIVE_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--VOLCENGINELIVE_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--VOLCENGINELIVE_ACCESSKEYID=\"your-access-key-id\" \\\n\t--VOLCENGINELIVE_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--VOLCENGINELIVE_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tAccessKeySecret:    fAccessKeySecret,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-tos/volcengine_tos.go",
    "content": "package volcenginetos\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/volcengine/ve-tos-golang-sdk/v2/tos\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n)\n\ntype DeployerConfig struct {\n\t// 火山引擎 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 火山引擎 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 火山引擎地域。\n\tRegion string `json:\"region\"`\n\t// 存储桶名。\n\tBucket string `json:\"bucket\"`\n\t// 自定义域名（不支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *tos.ClientV2\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tRegion:          config.Region,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.Bucket == \"\" {\n\t\treturn nil, errors.New(\"config `bucket` is required\")\n\t}\n\tif d.config.Domain == \"\" {\n\t\treturn nil, errors.New(\"config `domain` is required\")\n\t}\n\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 设置自定义域名\n\t// REF: https://www.volcengine.com/docs/6349/764779\n\tputBucketCustomDomainReq := &tos.PutBucketCustomDomainInput{\n\t\tBucket: d.config.Bucket,\n\t\tRule: tos.CustomDomainRule{\n\t\t\tDomain: d.config.Domain,\n\t\t\tCertID: upres.CertId,\n\t\t},\n\t}\n\tputBucketCustomDomainResp, err := d.sdkClient.PutBucketCustomDomain(ctx, putBucketCustomDomainReq)\n\td.logger.Debug(\"sdk request 'tos.PutBucketCustomDomain'\", slog.Any(\"request\", putBucketCustomDomainReq), slog.Any(\"response\", putBucketCustomDomainResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'tos.PutBucketCustomDomain': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*tos.ClientV2, error) {\n\tendpoint := fmt.Sprintf(\"tos-%s.volces.com\", region)\n\n\tclient, err := tos.NewClientV2(\n\t\tendpoint,\n\t\ttos.WithRegion(region),\n\t\ttos.WithCredentials(tos.NewStaticCredentials(accessKeyId, accessKeySecret)),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-tos/volcengine_tos_test.go",
    "content": "package volcenginetos_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-tos\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfBucket          string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"VOLCENGINETOS_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fBucket, argsPrefix+\"BUCKET\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./volcengine_tos_test.go -args \\\n\t--VOLCENGINETOS_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--VOLCENGINETOS_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--VOLCENGINETOS_ACCESSKEYID=\"your-access-key-id\" \\\n\t--VOLCENGINETOS_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--VOLCENGINETOS_REGION=\"cn-beijing\" \\\n\t--VOLCENGINETOS_BUCKET=\"your-tos-bucket\" \\\n\t--VOLCENGINETOS_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"BUCKET: %v\", fBucket),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegion:          fRegion,\n\t\t\tBucket:          fBucket,\n\t\t\tDomain:          fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-vod/consts.go",
    "content": "package volcenginevod\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n\t// 匹配模式：通配符匹配。\n\tDOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\"\n\t// 匹配模式：证书 SAN 匹配。\n\tDOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\"\n)\n\nconst (\n\t// 域名类型：点播加速域名。\n\tDOMAIN_TYPE_PLAY = \"play\"\n\t// 域名类型：封面加速域名。\n\tDOMAIN_TYPE_IMAGE = \"image\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-vod/volcengine_vod.go",
    "content": "package volcenginevod\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\tvevod \"github.com/volcengine/volc-sdk-golang/service/vod\"\n\tvevodbusiness \"github.com/volcengine/volc-sdk-golang/service/vod/models/business\"\n\tvevodrequest \"github.com/volcengine/volc-sdk-golang/service/vod/models/request\"\n\tve \"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\ntype DeployerConfig struct {\n\t// 火山引擎 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 火山引擎 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 点播空间名称。\n\tSpaceName string `json:\"spaceName\"`\n\t// 域名匹配模式。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 点播域名类型。\n\tDomainType string `json:\"domainType\"`\n\t// 点播加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *vevod.Vod\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient := vevod.NewInstance()\n\tclient.SetAccessKey(config.AccessKeyId)\n\tclient.SetSecretKey(config.AccessKeySecret)\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名\n\tdomains := make([]string, 0)\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tdomains = append(domains, d.config.Domain)\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_WILDCARD:\n\t\t{\n\t\t\tif d.config.Domain == \"\" {\n\t\t\t\treturn nil, errors.New(\"config `domain` is required\")\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(d.config.Domain, \"*.\") {\n\t\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\t\treturn xcerthostname.IsMatch(d.config.Domain, domain)\n\t\t\t\t})\n\t\t\t\tif len(domains) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"could not find any domains matched by wildcard\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdomains = append(domains, d.config.Domain)\n\t\t\t}\n\t\t}\n\n\tcase DOMAIN_MATCH_PATTERN_CERTSAN:\n\t\t{\n\t\t\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomainCandidates, err := d.getAllDomains(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdomains = lo.Filter(domainCandidates, func(domain string, _ int) bool {\n\t\t\t\treturn certX509.VerifyHostname(domain) == nil\n\t\t\t})\n\t\t\tif len(domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"could not find any domains matched by certificate\")\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 遍历更新域名证书\n\tif len(domains) == 0 {\n\t\td.logger.Info(\"no vod domains to deploy\")\n\t} else {\n\t\td.logger.Info(\"found vod domains to deploy\", slog.Any(\"domains\", domains))\n\t\tvar errs []error\n\n\t\tfor _, domain := range domains {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t\tif err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\treturn nil, errors.Join(errs...)\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {\n\tdomains := make([]string, 0)\n\n\t// 获取空间域名列表\n\t// REF: https://www.volcengine.com/docs/4/106062\n\tlistDomainDetailOffset := 0\n\tlistDomainDetailLimit := 1000\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tlistDomainReq := &vevodrequest.VodListDomainRequest{\n\t\t\tSpaceName:         d.config.SpaceName,\n\t\t\tDomainType:        d.config.DomainType,\n\t\t\tSourceStationType: 1,\n\t\t\tOffset:            int32(listDomainDetailOffset),\n\t\t\tLimit:             int32(listDomainDetailLimit),\n\t\t}\n\t\tlistDomainResp, _, err := d.sdkClient.ListDomain(listDomainReq)\n\t\td.logger.Debug(\"sdk request 'vod.ListDomain'\", slog.Any(\"request\", listDomainReq), slog.Any(\"response\", listDomainResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'vod.ListDomain': %w\", err)\n\t\t}\n\n\t\tif listDomainResp.Result == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tvar domainInstances []*vevodbusiness.VodDomainInstanceInfo\n\t\tswitch d.config.DomainType {\n\t\tcase DOMAIN_TYPE_PLAY:\n\t\t\tdomainInstances = listDomainResp.GetResult().GetPlayInstanceInfo().GetByteInstances()\n\t\tcase DOMAIN_TYPE_IMAGE:\n\t\t\tdomainInstances = listDomainResp.GetResult().GetImageInstanceInfo().GetByteInstances()\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"unsupported domain type: '%s'\", d.config.DomainType)\n\t\t}\n\n\t\tfor _, domainInstance := range domainInstances {\n\t\t\tif domainInstance.Domains == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, domainItem := range domainInstance.Domains {\n\t\t\t\tdomains = append(domains, domainItem.Domain)\n\t\t\t}\n\t\t}\n\n\t\tif listDomainResp.Result.Total <= int64(listDomainDetailOffset+listDomainDetailLimit) {\n\t\t\tbreak\n\t\t}\n\n\t\tlistDomainDetailOffset += listDomainDetailLimit\n\t}\n\n\treturn domains, nil\n}\n\nfunc (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error {\n\t// 更新域名配置\n\t// REF: https://www.volcengine.com/docs/4/1317310\n\tupdateDomainConfigReq := &vevodrequest.VodUpdateDomainConfigRequest{\n\t\tSpaceName:  d.config.SpaceName,\n\t\tDomainType: d.config.DomainType,\n\t\tDomain:     domain,\n\t\tConfig: &vevodbusiness.VodDomainConfig{\n\t\t\tHTTPS: &vevodbusiness.HTTPS{\n\t\t\t\tSwitch: ve.Bool(true),\n\t\t\t\tCertInfo: &vevodbusiness.CertInfo{\n\t\t\t\t\tCertId: &cloudCertId,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tupdateDomainConfigResp, _, err := d.sdkClient.UpdateDomainConfig(updateDomainConfigReq)\n\td.logger.Debug(\"sdk request 'vod.UpdateDomainConfig'\", slog.Any(\"request\", updateDomainConfigReq), slog.Any(\"response\", updateDomainConfigResp))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-vod/volcengine_vod_test.go",
    "content": "package volcenginevod_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-vod\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfSpaceName       string\n\tfDomainType      string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"VOLCENGINEVOD_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fSpaceName, argsPrefix+\"SPACENAME\", \"\", \"\")\n\tflag.StringVar(&fDomainType, argsPrefix+\"DOMAINTYPE\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./volcengine_vod_test.go -args \\\n\t--VOLCENGINEVOD_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--VOLCENGINEVOD_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--VOLCENGINEVOD_ACCESSKEYID=\"your-access-key-id\" \\\n\t--VOLCENGINEVOD_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--VOLCENGINEVOD_SPACENAME=\"vod-space-name\" \\\n\t--VOLCENGINEVOD_DOMAINTYPE=\"play\" \\\n\t--VOLCENGINEVOD_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"SPACENAME: %v\", fSpaceName),\n\t\t\tfmt.Sprintf(\"DOMAINTYPE: %v\", fDomainType),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:        fAccessKeyId,\n\t\t\tAccessKeySecret:    fAccessKeySecret,\n\t\t\tDomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,\n\t\t\tSpaceName:          fSpaceName,\n\t\t\tDomainType:         fDomainType,\n\t\t\tDomain:             fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-waf/consts.go",
    "content": "package volcenginewaf\n\nconst (\n\t// 接入模式：CNAME 接入。\n\tACCESS_MODE_CNAME = \"cname\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-waf/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"github.com/volcengine/volcengine-go-sdk/service/waf\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/client\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/client/metadata\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/corehandlers\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/request\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/signer/volc\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine/volcenginequery\"\n)\n\n// This is a partial copy of https://github.com/volcengine/volcengine-go-sdk/blob/master/service/waf/service_waf.go\n// to lightweight the vendor packages in the built binary.\ntype WafClient struct {\n\t*client.Client\n}\n\nfunc NewWafClient(p client.ConfigProvider, cfgs ...*volcengine.Config) *WafClient {\n\tc := p.ClientConfig(waf.EndpointsID, cfgs...)\n\treturn newDcdnClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, c.SigningName)\n}\n\nfunc newDcdnClient(cfg volcengine.Config, handlers request.Handlers, endpoint, signingRegion, signingName string) *WafClient {\n\tsvc := &WafClient{\n\t\tClient: client.New(\n\t\t\tcfg,\n\t\t\tmetadata.ClientInfo{\n\t\t\t\tServiceName:   waf.ServiceName,\n\t\t\t\tServiceID:     waf.ServiceID,\n\t\t\t\tSigningName:   signingName,\n\t\t\t\tSigningRegion: signingRegion,\n\t\t\t\tEndpoint:      endpoint,\n\t\t\t\tAPIVersion:    \"2023-12-25\",\n\t\t\t},\n\t\t\thandlers,\n\t\t),\n\t}\n\n\tsvc.Handlers.Build.PushBackNamed(corehandlers.SDKVersionUserAgentHandler)\n\tsvc.Handlers.Build.PushBackNamed(corehandlers.AddHostExecEnvUserAgentHandler)\n\tsvc.Handlers.Sign.PushBackNamed(volc.SignRequestHandler)\n\tsvc.Handlers.Build.PushBackNamed(volcenginequery.BuildHandler)\n\tsvc.Handlers.Unmarshal.PushBackNamed(volcenginequery.UnmarshalHandler)\n\tsvc.Handlers.UnmarshalMeta.PushBackNamed(volcenginequery.UnmarshalMetaHandler)\n\tsvc.Handlers.UnmarshalError.PushBackNamed(volcenginequery.UnmarshalErrorHandler)\n\n\treturn svc\n}\n\nfunc (c *WafClient) newRequest(op *request.Operation, params, data interface{}) *request.Request {\n\treq := c.NewRequest(op, params, data)\n\n\treturn req\n}\n\nfunc (c *WafClient) UpdateDomain(input *waf.UpdateDomainInput) (*waf.UpdateDomainOutput, error) {\n\treq, out := c.UpdateDomainRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *WafClient) UpdateDomainRequest(input *waf.UpdateDomainInput) (req *request.Request, output *waf.UpdateDomainOutput) {\n\top := &request.Operation{\n\t\tName:       \"UpdateDomain\",\n\t\tHTTPMethod: \"POST\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &waf.UpdateDomainInput{}\n\t}\n\n\toutput = &waf.UpdateDomainOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treq.HTTPRequest.Header.Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\n\treturn\n}\n\nfunc (c *WafClient) ListDomain(input *waf.ListDomainInput) (*waf.ListDomainOutput, error) {\n\treq, out := c.ListDomainRequest(input)\n\treturn out, req.Send()\n}\n\nfunc (c *WafClient) ListDomainRequest(input *waf.ListDomainInput) (req *request.Request, output *waf.ListDomainOutput) {\n\top := &request.Operation{\n\t\tName:       \"ListDomain\",\n\t\tHTTPMethod: \"POST\",\n\t\tHTTPPath:   \"/\",\n\t}\n\n\tif input == nil {\n\t\tinput = &waf.ListDomainInput{}\n\t}\n\n\toutput = &waf.ListDomainOutput{}\n\treq = c.newRequest(op, input, output)\n\n\treq.HTTPRequest.Header.Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-waf/volcengine_waf.go",
    "content": "package volcenginewaf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\tvewaf \"github.com/volcengine/volcengine-go-sdk/service/waf\"\n\tve \"github.com/volcengine/volcengine-go-sdk/volcengine\"\n\tvesession \"github.com/volcengine/volcengine-go-sdk/volcengine/session\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-waf/internal\"\n)\n\ntype DeployerConfig struct {\n\t// 火山引擎 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 火山引擎 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 火山引擎地域。\n\tRegion string `json:\"region\"`\n\t// WAF 接入模式。\n\tAccessMode string `json:\"accessMode\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *internal.WafClient\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t\tRegion:          config.Region,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n\n\td.sdkCertmgr.SetLogger(logger)\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 根据接入方式决定部署方式\n\tswitch d.config.AccessMode {\n\tcase ACCESS_MODE_CNAME:\n\t\tif err := d.deployWithCNAME(ctx, upres.CertId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported access mode '%s'\", d.config.AccessMode)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc (d *Deployer) deployWithCNAME(ctx context.Context, cloudCertId string) error {\n\tif d.config.Domain == \"\" {\n\t\treturn errors.New(\"config `domain` is required\")\n\t}\n\n\t// 查询云 WAF 实例防护网站信息\n\t// REF: https://www.volcengine.com/docs/6511/1214827\n\tlistDomainReq := &vewaf.ListDomainInput{\n\t\tRegion:        ve.String(d.config.Region),\n\t\tDomain:        ve.String(d.config.Domain),\n\t\tAccurateQuery: ve.Int32(1),\n\t\tPage:          ve.Int32(1),\n\t\tPageSize:      ve.Int32(1),\n\t}\n\tlistDomainResp, err := d.sdkClient.ListDomain(listDomainReq)\n\td.logger.Debug(\"sdk request 'waf.ListDomain'\", slog.Any(\"request\", listDomainReq), slog.Any(\"response\", listDomainResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'waf.ListDomain': %w\", err)\n\t} else if len(listDomainResp.Data) == 0 {\n\t\treturn fmt.Errorf(\"could not find domain '%s'\", d.config.Domain)\n\t}\n\n\t// 更新云 WAF 实例的防护网站信息\n\t// REF: https://www.volcengine.com/docs/6511/1214835\n\tdomainInfo := listDomainResp.Data[0]\n\tupdateDomainReq := &vewaf.UpdateDomainInput{\n\t\tRegion:     ve.String(d.config.Region),\n\t\tDomain:     ve.String(d.config.Domain),\n\t\tAccessMode: ve.Int32(10),\n\t\tProtocols:  ve.StringSlice([]string{\"HTTP\", \"HTTPS\"}),\n\t\tProtocolPorts: &vewaf.ProtocolPortsForUpdateDomainInput{\n\t\t\tHTTP:  ve.Int32Slice([]int32{80}),\n\t\t\tHTTPS: ve.Int32Slice([]int32{443}),\n\t\t},\n\t\tVolcCertificateID:   ve.String(cloudCertId),\n\t\tCertificatePlatform: ve.String(\"certificate-service\"),\n\t}\n\tif domainInfo.Protocols != nil {\n\t\tprotocols := strings.Split(ve.StringValue(domainInfo.Protocols), \",\")\n\t\tif !lo.Contains(protocols, \"HTTPS\") {\n\t\t\tprotocols = append(protocols, \"HTTPS\")\n\t\t}\n\t\tupdateDomainReq.Protocols = ve.StringSlice(protocols)\n\t}\n\tif domainInfo.ProtocolPorts != nil {\n\t\tupdateDomainReq.ProtocolPorts.HTTP = domainInfo.ProtocolPorts.HTTP\n\t\tif domainInfo.ProtocolPorts.HTTPS != nil {\n\t\t\tupdateDomainReq.ProtocolPorts.HTTPS = domainInfo.ProtocolPorts.HTTPS\n\t\t}\n\t}\n\tupdateDomainResp, err := d.sdkClient.UpdateDomain(updateDomainReq)\n\td.logger.Debug(\"sdk request 'waf.UpdateDomain'\", slog.Any(\"request\", updateDomainReq), slog.Any(\"response\", updateDomainResp))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute sdk request 'waf.UpdateDomain': %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.WafClient, error) {\n\tconfig := ve.NewConfig().\n\t\tWithAkSk(accessKeyId, accessKeySecret).\n\t\tWithRegion(region)\n\n\tsession, err := vesession.NewSession(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := internal.NewWafClient(session)\n\treturn client, nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/volcengine-waf/volcengine_waf_test.go",
    "content": "package volcenginewaf_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-waf\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfRegion          string\n\tfAccessMode      string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"VOLCENGINEWAF_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fRegion, argsPrefix+\"REGION\", \"\", \"\")\n\tflag.StringVar(&fAccessMode, argsPrefix+\"ACCESSMODE\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./volcengine_waf_test.go -args \\\n\t--VOLCENGINEWAF_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--VOLCENGINEWAF_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--VOLCENGINEWAF_ACCESSKEYID=\"your-access-key-id\" \\\n\t--VOLCENGINEWAF_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--VOLCENGINEWAF_REGION=\"cn-beijing\" \\\n\t--VOLCENGINEWAF_ACCESSMODE=\"cname\" \\\n\t--VOLCENGINEWAF_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"REGION: %v\", fRegion),\n\t\t\tfmt.Sprintf(\"ACCESSMODE: %v\", fAccessMode),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tRegion:          fRegion,\n\t\t\tAccessMode:      fAccessMode,\n\t\t\tDomain:          fDomain,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/wangsu-cdn/consts.go",
    "content": "package wangsucdn\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn.go",
    "content": "package wangsucdn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/wangsu-certificate\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\twangsusdk \"github.com/certimate-go/certimate/pkg/sdk3rd/wangsu/cdn\"\n)\n\ntype DeployerConfig struct {\n\t// 网宿云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 网宿云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 域名匹配模式。暂时只支持精确匹配。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名数组（支持泛域名）。\n\tDomains []string `json:\"domains\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *wangsusdk.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 上传证书\n\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t} else {\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t}\n\n\t// 获取待部署的域名列表\n\tdomains := make([]string, 0)\n\tswitch d.config.DomainMatchPattern {\n\tcase \"\", DOMAIN_MATCH_PATTERN_EXACT:\n\t\t{\n\t\t\tif len(d.config.Domains) == 0 {\n\t\t\t\treturn nil, errors.New(\"config `domains` is required\")\n\t\t\t}\n\n\t\t\t// \"*.example.com\" → \".example.com\"，适配网宿云 CDN 要求的泛域名格式\n\t\t\tdomains = lo.Map(d.config.Domains, func(domain string, _ int) string {\n\t\t\t\treturn strings.TrimPrefix(domain, \"*\")\n\t\t\t})\n\t\t}\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported domain match pattern: '%s'\", d.config.DomainMatchPattern)\n\t}\n\n\t// 批量修改域名证书配置\n\t// REF: https://www.wangsu.com/document/api-doc/37447\n\tcertId, _ := strconv.ParseInt(upres.CertId, 10, 64)\n\tbatchUpdateCertificateConfigReq := &wangsusdk.BatchUpdateCertificateConfigRequest{\n\t\tCertificateId: certId,\n\t\tDomainNames:   domains,\n\t}\n\tbatchUpdateCertificateConfigResp, err := d.sdkClient.BatchUpdateCertificateConfigWithContext(ctx, batchUpdateCertificateConfigReq)\n\td.logger.Debug(\"sdk request 'cdn.BatchUpdateCertificateConfig'\", slog.Any(\"request\", batchUpdateCertificateConfigReq), slog.Any(\"response\", batchUpdateCertificateConfigResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdn.BatchUpdateCertificateConfig': %w\", err)\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret string) (*wangsusdk.Client, error) {\n\treturn wangsusdk.NewClient(accessKeyId, accessKeySecret)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn_test.go",
    "content": "package wangsucdn_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/wangsu-cdn\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfDomain          string\n)\n\nfunc init() {\n\targsPrefix := \"WANGSUCDN_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./wangsu_cdn_test.go -args \\\n\t--WANGSUCDN_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--WANGSUCDN_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--WANGSUCDN_ACCESSKEYID=\"your-access-key-id\" \\\n\t--WANGSUCDN_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--WANGSUCDN_DOMAIN=\"example.com\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tDomains:         []string{fDomain},\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/wangsu-cdnpro/consts.go",
    "content": "package wangsucdnpro\n\nconst (\n\t// 匹配模式：精确匹配。\n\tDOMAIN_MATCH_PATTERN_EXACT = \"exact\"\n)\n"
  },
  {
    "path": "pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go",
    "content": "package wangsucdnpro\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\twangsucdn \"github.com/certimate-go/certimate/pkg/sdk3rd/wangsu/cdnpro\"\n\txwait \"github.com/certimate-go/certimate/pkg/utils/wait\"\n)\n\ntype DeployerConfig struct {\n\t// 网宿云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 网宿云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 网宿云 API Key。\n\tApiKey string `json:\"apiKey\"`\n\t// 网宿云环境。\n\tEnvironment string `json:\"environment\"`\n\t// 域名匹配模式。暂时只支持精确匹配。\n\t// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。\n\tDomainMatchPattern string `json:\"domainMatchPattern,omitempty\"`\n\t// 加速域名（支持泛域名）。\n\tDomain string `json:\"domain\"`\n\t// 证书 ID。\n\t// 选填。零值时表示新建证书；否则表示更新证书。\n\tCertificateId string `json:\"certificateId,omitempty\"`\n\t// Webhook ID。\n\t// 选填。\n\tWebhookId string `json:\"webhookId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig    *DeployerConfig\n\tlogger    *slog.Logger\n\tsdkClient *wangsucdn.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:    config,\n\t\tlogger:    slog.Default(),\n\t\tsdkClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.Domain == \"\" {\n\t\treturn nil, errors.New(\"config `domain` is required\")\n\t}\n\n\t// 查询已部署加速域名的详情\n\tgetHostnameDetailResp, err := d.sdkClient.GetHostnameDetailWithContext(ctx, d.config.Domain)\n\td.logger.Debug(\"sdk request 'cdnpro.GetHostnameDetail'\", slog.String(\"hostname\", d.config.Domain), slog.Any(\"response\", getHostnameDetailResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdnpro.GetHostnameDetail': %w\", err)\n\t}\n\n\t// 生成网宿云证书参数\n\tencryptedPrivateKey, err := encryptPrivateKey(privkeyPEM, d.config.ApiKey, time.Now().Unix())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to encrypt private key: %w\", err)\n\t}\n\tcertificateNewVersionInfo := &wangsucdn.CertificateVersionInfo{\n\t\tPrivateKey:  lo.ToPtr(encryptedPrivateKey),\n\t\tCertificate: lo.ToPtr(certPEM),\n\t}\n\n\t// 网宿云证书 URL 中包含证书 ID 及版本号\n\t// 格式：\n\t//    http://open.chinanetcenter.com/cdn/certificates/5dca2205f9e9cc0001df7b33\n\t//    http://open.chinanetcenter.com/cdn/certificates/329f12c1fe6708c23c31e91f/versions/5\n\tvar wangsuCertUrl string\n\tvar wangsuCertId string\n\tvar wangsuCertVer int32\n\n\t// 如果原证书 ID 为空，则创建证书；否则更新证书。\n\ttimestamp := time.Now().Unix()\n\tif d.config.CertificateId == \"\" {\n\t\t// 创建证书\n\t\tcreateCertificateReq := &wangsucdn.CreateCertificateRequest{\n\t\t\tTimestamp:  timestamp,\n\t\t\tName:       lo.ToPtr(fmt.Sprintf(\"certimate_%d\", time.Now().UnixMilli())),\n\t\t\tAutoRenew:  lo.ToPtr(\"Off\"),\n\t\t\tNewVersion: certificateNewVersionInfo,\n\t\t}\n\t\tcreateCertificateResp, err := d.sdkClient.CreateCertificateWithContext(ctx, createCertificateReq)\n\t\td.logger.Debug(\"sdk request 'cdnpro.CreateCertificate'\", slog.Any(\"request\", createCertificateReq), slog.Any(\"response\", createCertificateResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdnpro.CreateCertificate': %w\", err)\n\t\t}\n\n\t\twangsuCertUrl = createCertificateResp.CertificateLocation\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"certUrl\", wangsuCertUrl))\n\n\t\twangsuCertIdMatches := regexp.MustCompile(`/certificates/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl)\n\t\tif len(wangsuCertIdMatches) > 1 {\n\t\t\twangsuCertId = wangsuCertIdMatches[1]\n\t\t}\n\n\t\twangsuCertVer = 1\n\t} else {\n\t\t// 更新证书\n\t\tupdateCertificateReq := &wangsucdn.UpdateCertificateRequest{\n\t\t\tTimestamp:  timestamp,\n\t\t\tName:       lo.ToPtr(fmt.Sprintf(\"certimate_%d\", time.Now().UnixMilli())),\n\t\t\tAutoRenew:  lo.ToPtr(\"Off\"),\n\t\t\tNewVersion: certificateNewVersionInfo,\n\t\t}\n\t\tupdateCertificateResp, err := d.sdkClient.UpdateCertificateWithContext(ctx, d.config.CertificateId, updateCertificateReq)\n\t\td.logger.Debug(\"sdk request 'cdnpro.CreateCertificate'\", slog.Any(\"certificateId\", d.config.CertificateId), slog.Any(\"request\", updateCertificateReq), slog.Any(\"response\", updateCertificateResp))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdnpro.UpdateCertificate': %w\", err)\n\t\t}\n\n\t\twangsuCertUrl = updateCertificateResp.CertificateLocation\n\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"certUrl\", wangsuCertUrl))\n\n\t\twangsuCertIdMatches := regexp.MustCompile(`/certificates/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl)\n\t\tif len(wangsuCertIdMatches) > 1 {\n\t\t\twangsuCertId = wangsuCertIdMatches[1]\n\t\t}\n\n\t\twangsuCertVerMatches := regexp.MustCompile(`/versions/(\\d+)`).FindStringSubmatch(wangsuCertUrl)\n\t\tif len(wangsuCertVerMatches) > 1 {\n\t\t\tn, _ := strconv.ParseInt(wangsuCertVerMatches[1], 10, 32)\n\t\t\twangsuCertVer = int32(n)\n\t\t}\n\t}\n\n\t// 创建部署任务\n\t// REF: https://www.wangsu.com/document/api-doc/27034\n\tvar wangsuTaskId string\n\tcreateDeploymentTaskReq := &wangsucdn.CreateDeploymentTaskRequest{\n\t\tName:   lo.ToPtr(fmt.Sprintf(\"certimate_%d\", time.Now().UnixMilli())),\n\t\tTarget: lo.ToPtr(d.config.Environment),\n\t\tActions: &[]wangsucdn.DeploymentTaskActionInfo{\n\t\t\t{\n\t\t\t\tAction:        lo.ToPtr(\"deploy_cert\"),\n\t\t\t\tCertificateId: lo.ToPtr(wangsuCertId),\n\t\t\t\tVersion:       lo.ToPtr(wangsuCertVer),\n\t\t\t},\n\t\t},\n\t}\n\tif d.config.WebhookId != \"\" {\n\t\tcreateDeploymentTaskReq.Webhook = lo.ToPtr(d.config.WebhookId)\n\t}\n\tcreateDeploymentTaskResp, err := d.sdkClient.CreateDeploymentTaskWithContext(ctx, createDeploymentTaskReq)\n\td.logger.Debug(\"sdk request 'cdnpro.CreateCertificate'\", slog.Any(\"request\", createDeploymentTaskReq), slog.Any(\"response\", createDeploymentTaskResp))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute sdk request 'cdnpro.CreateDeploymentTask': %w\", err)\n\t} else {\n\t\twangsuTaskMatches := regexp.MustCompile(`/deploymentTasks/([a-zA-Z0-9-]+)`).FindStringSubmatch(createDeploymentTaskResp.DeploymentTaskLocation)\n\t\tif len(wangsuTaskMatches) > 1 {\n\t\t\twangsuTaskId = wangsuTaskMatches[1]\n\t\t}\n\t}\n\n\t// 获取部署任务详细信息，等待任务状态变更\n\t// REF: https://www.wangsu.com/document/api-doc/27038\n\tif _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) {\n\t\tgetDeploymentTaskDetailResp, err := d.sdkClient.GetDeploymentTaskDetailWithContext(ctx, wangsuTaskId)\n\t\td.logger.Info(\"sdk request 'cdnpro.GetDeploymentTaskDetail'\", slog.Any(\"taskId\", wangsuTaskId), slog.Any(\"response\", getDeploymentTaskDetailResp))\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to execute sdk request 'cdnpro.GetDeploymentTaskDetail': %w\", err)\n\t\t}\n\n\t\tif getDeploymentTaskDetailResp.Status == \"failed\" {\n\t\t\treturn false, fmt.Errorf(\"unexpected wangsu deployment task status\")\n\t\t} else if getDeploymentTaskDetailResp.Status == \"succeeded\" || getDeploymentTaskDetailResp.FinishTime != \"\" {\n\t\t\treturn true, nil\n\t\t}\n\n\t\td.logger.Info(fmt.Sprintf(\"waiting for wangsu deployment task completion (current status: %s) ...\", getDeploymentTaskDetailResp.Status))\n\t\treturn false, nil\n\t}, time.Second*5); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret string) (*wangsucdn.Client, error) {\n\treturn wangsucdn.NewClient(accessKeyId, accessKeySecret)\n}\n\nfunc encryptPrivateKey(privkeyPEM string, apiKey string, timestamp int64) (string, error) {\n\tdate := time.Unix(timestamp, 0).UTC()\n\tdateStr := date.Format(\"Mon, 02 Jan 2006 15:04:05 GMT\")\n\n\th := hmac.New(sha256.New, []byte(apiKey))\n\th.Write([]byte(dateStr))\n\taesivkey := h.Sum(nil)\n\taesivkeyHex := hex.EncodeToString(aesivkey)\n\n\tif len(aesivkeyHex) != 64 {\n\t\treturn \"\", fmt.Errorf(\"invalid hmac length: %d\", len(aesivkeyHex))\n\t}\n\tivHex := aesivkeyHex[:32]\n\tkeyHex := aesivkeyHex[32:64]\n\n\tiv, err := hex.DecodeString(ivHex)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to decode iv: %w\", err)\n\t}\n\n\tkey, err := hex.DecodeString(keyHex)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to decode key: %w\", err)\n\t}\n\n\tblock, err := aes.NewCipher(key)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tplainBytes := []byte(privkeyPEM)\n\tpadlen := aes.BlockSize - len(plainBytes)%aes.BlockSize\n\tif padlen > 0 {\n\t\tpaddata := bytes.Repeat([]byte{byte(padlen)}, padlen)\n\t\tplainBytes = append(plainBytes, paddata...)\n\t}\n\n\tencBytes := make([]byte, len(plainBytes))\n\tmode := cipher.NewCBCEncrypter(block, iv)\n\tmode.CryptBlocks(encBytes, plainBytes)\n\n\treturn base64.StdEncoding.EncodeToString(encBytes), nil\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro_test.go",
    "content": "package wangsucdnpro_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/wangsu-cdnpro\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfApiKey          string\n\tfEnvironment     string\n\tfDomain          string\n\tfCertificateId   string\n\tfWebhookId       string\n)\n\nfunc init() {\n\targsPrefix := \"WANGSUCDNPRO_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fApiKey, argsPrefix+\"APIKEY\", \"\", \"\")\n\tflag.StringVar(&fEnvironment, argsPrefix+\"ENVIRONMENT\", \"production\", \"\")\n\tflag.StringVar(&fDomain, argsPrefix+\"DOMAIN\", \"\", \"\")\n\tflag.StringVar(&fCertificateId, argsPrefix+\"CERTIFICATEID\", \"\", \"\")\n\tflag.StringVar(&fWebhookId, argsPrefix+\"WEBHOOKID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./wangsu_cdnpro_test.go -args \\\n\t--WANGSUCDNPRO_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--WANGSUCDNPRO_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--WANGSUCDNPRO_ACCESSKEYID=\"your-access-key-id\" \\\n\t--WANGSUCDNPRO_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--WANGSUCDNPRO_APIKEY=\"your-api-key\" \\\n\t--WANGSUCDNPRO_ENVIRONMENT=\"production\" \\\n\t--WANGSUCDNPRO_DOMAIN=\"example.com\" \\\n\t--WANGSUCDNPRO_CERTIFICATEID=\"your-certificate-id\" \\\n\t--WANGSUCDNPRO_WEBHOOKID=\"your-webhook-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"APIKEY: %v\", fApiKey),\n\t\t\tfmt.Sprintf(\"ENVIRONMENT: %v\", fEnvironment),\n\t\t\tfmt.Sprintf(\"DOMAIN: %v\", fDomain),\n\t\t\tfmt.Sprintf(\"CERTIFICATEID: %v\", fCertificateId),\n\t\t\tfmt.Sprintf(\"WEBHOOKID: %v\", fWebhookId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tApiKey:          fApiKey,\n\t\t\tEnvironment:     fEnvironment,\n\t\t\tDomain:          fDomain,\n\t\t\tCertificateId:   fCertificateId,\n\t\t\tWebhookId:       fWebhookId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate.go",
    "content": "package wangsucertificate\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/certmgr\"\n\tmcertmgr \"github.com/certimate-go/certimate/pkg/core/certmgr/providers/wangsu-certificate\"\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\twangsusdk \"github.com/certimate-go/certimate/pkg/sdk3rd/wangsu/certificate\"\n)\n\ntype DeployerConfig struct {\n\t// 网宿云 AccessKeyId。\n\tAccessKeyId string `json:\"accessKeyId\"`\n\t// 网宿云 AccessKeySecret。\n\tAccessKeySecret string `json:\"accessKeySecret\"`\n\t// 证书 ID。\n\t// 选填。零值时表示新建证书；否则表示更新证书。\n\tCertificateId string `json:\"certificateId,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\tsdkClient  *wangsusdk.Client\n\tsdkCertmgr certmgr.Provider\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create client: %w\", err)\n\t}\n\n\tpcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{\n\t\tAccessKeyId:     config.AccessKeyId,\n\t\tAccessKeySecret: config.AccessKeySecret,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create certmgr: %w\", err)\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\tsdkClient:  client,\n\t\tsdkCertmgr: pcertmgr,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\tif d.config.CertificateId == \"\" {\n\t\t// 上传证书\n\t\tupres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to upload certificate file: %w\", err)\n\t\t} else {\n\t\t\td.logger.Info(\"ssl certificate uploaded\", slog.Any(\"result\", upres))\n\t\t}\n\t} else {\n\t\t// 替换证书\n\t\topres, err := d.sdkCertmgr.Replace(ctx, d.config.CertificateId, certPEM, privkeyPEM)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to replace certificate file: %w\", err)\n\t\t} else {\n\t\t\td.logger.Info(\"ssl certificate replaced\", slog.Any(\"result\", opres))\n\t\t}\n\t}\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc createSDKClient(accessKeyId, accessKeySecret string) (*wangsusdk.Client, error) {\n\treturn wangsusdk.NewClient(accessKeyId, accessKeySecret)\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate_test.go",
    "content": "package wangsucertificate_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/wangsu-certificate\"\n)\n\nvar (\n\tfInputCertPath   string\n\tfInputKeyPath    string\n\tfAccessKeyId     string\n\tfAccessKeySecret string\n\tfCertificateId   string\n)\n\nfunc init() {\n\targsPrefix := \"WANGSUCERTIFICATE_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fAccessKeyId, argsPrefix+\"ACCESSKEYID\", \"\", \"\")\n\tflag.StringVar(&fAccessKeySecret, argsPrefix+\"ACCESSKEYSECRET\", \"\", \"\")\n\tflag.StringVar(&fCertificateId, argsPrefix+\"CERTIFICATEID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./wangsu_certificate_test.go -args \\\n\t--WANGSUCERTIFICATE_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--WANGSUCERTIFICATE_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--WANGSUCERTIFICATE_ACCESSKEYID=\"your-access-key-id\" \\\n\t--WANGSUCERTIFICATE_ACCESSKEYSECRET=\"your-access-key-secret\" \\\n\t--WANGSUCERTIFICATE_CERTIFICATEID=\"your-certificate-id\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"ACCESSKEYID: %v\", fAccessKeyId),\n\t\t\tfmt.Sprintf(\"ACCESSKEYSECRET: %v\", fAccessKeySecret),\n\t\t\tfmt.Sprintf(\"CERTIFICATEID: %v\", fCertificateId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tAccessKeyId:     fAccessKeyId,\n\t\t\tAccessKeySecret: fAccessKeySecret,\n\t\t\tCertificateId:   fCertificateId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/webhook/webhook.go",
    "content": "package webhook\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/deployer\"\n\txcert \"github.com/certimate-go/certimate/pkg/utils/cert\"\n\txcertx509 \"github.com/certimate-go/certimate/pkg/utils/cert/x509\"\n)\n\ntype DeployerConfig struct {\n\t// Webhook URL。\n\tWebhookUrl string `json:\"webhookUrl\"`\n\t// Webhook 回调数据（application/json 或 application/x-www-form-urlencoded 格式）。\n\tWebhookData string `json:\"webhookData,omitempty\"`\n\t// 请求谓词。\n\t// 零值时默认值 \"POST\"。\n\tMethod string `json:\"method,omitempty\"`\n\t// 请求标头。\n\tHeaders map[string]string `json:\"headers,omitempty\"`\n\t// 请求超时（单位：秒）。\n\t// 零值时默认值 30。\n\tTimeout int `json:\"timeout,omitempty\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype Deployer struct {\n\tconfig     *DeployerConfig\n\tlogger     *slog.Logger\n\thttpClient *resty.Client\n}\n\nvar _ deployer.Provider = (*Deployer)(nil)\n\nfunc NewDeployer(config *DeployerConfig) (*Deployer, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the deployer provider is nil\")\n\t}\n\n\tclient := resty.New().\n\t\tSetTimeout(30 * time.Second).\n\t\tSetRetryCount(3).\n\t\tSetRetryWaitTime(5 * time.Second)\n\tif config.Timeout > 0 {\n\t\tclient.SetTimeout(time.Duration(config.Timeout) * time.Second)\n\t}\n\tif config.AllowInsecureConnections {\n\t\tclient.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn &Deployer{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\thttpClient: client,\n\t}, nil\n}\n\nfunc (d *Deployer) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\td.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\td.logger = logger\n\t}\n}\n\nfunc (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {\n\t// 解析证书内容\n\tcertX509, err := xcert.ParseCertificateFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse x509: %w\", err)\n\t}\n\n\t// 提取服务器证书和中间证书\n\tserverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to extract certs: %w\", err)\n\t}\n\n\t// 处理 Webhook URL\n\twebhookUrl, err := url.Parse(d.config.WebhookUrl)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse webhook url: %w\", err)\n\t} else if webhookUrl.Scheme != \"http\" && webhookUrl.Scheme != \"https\" {\n\t\treturn nil, fmt.Errorf(\"unsupported webhook url scheme '%s'\", webhookUrl.Scheme)\n\t}\n\n\t// 处理 Webhook 请求谓词\n\twebhookMethod := strings.ToUpper(d.config.Method)\n\tif webhookMethod == \"\" {\n\t\twebhookMethod = http.MethodPost\n\t} else if webhookMethod != http.MethodGet &&\n\t\twebhookMethod != http.MethodPost &&\n\t\twebhookMethod != http.MethodPut &&\n\t\twebhookMethod != http.MethodPatch &&\n\t\twebhookMethod != http.MethodDelete {\n\t\treturn nil, fmt.Errorf(\"unsupported webhook request method '%s'\", webhookMethod)\n\t}\n\n\t// 处理 Webhook 请求标头\n\twebhookHeaders := make(http.Header)\n\tfor k, v := range d.config.Headers {\n\t\twebhookHeaders.Set(k, v)\n\t}\n\n\t// 处理 Webhook 请求内容类型\n\tconst CONTENT_TYPE_JSON = \"application/json\"\n\tconst CONTENT_TYPE_FORM = \"application/x-www-form-urlencoded\"\n\tconst CONTENT_TYPE_MULTIPART = \"multipart/form-data\"\n\twebhookContentType := webhookHeaders.Get(\"Content-Type\")\n\tif webhookContentType == \"\" {\n\t\twebhookContentType = CONTENT_TYPE_JSON\n\t\twebhookHeaders.Set(\"Content-Type\", CONTENT_TYPE_JSON)\n\t} else if strings.HasPrefix(webhookContentType, CONTENT_TYPE_JSON) &&\n\t\tstrings.HasPrefix(webhookContentType, CONTENT_TYPE_FORM) &&\n\t\tstrings.HasPrefix(webhookContentType, CONTENT_TYPE_MULTIPART) {\n\t\treturn nil, fmt.Errorf(\"unsupported webhook content type '%s'\", webhookContentType)\n\t}\n\n\t// 处理 Webhook 请求数据\n\tvar webhookData interface{}\n\tif d.config.WebhookData == \"\" {\n\t\twebhookData = map[string]string{\n\t\t\t\"name\":    strings.Join(xcertx509.GetSubjectAltNames(certX509), \";\"),\n\t\t\t\"cert\":    certPEM,\n\t\t\t\"privkey\": privkeyPEM,\n\t\t}\n\t} else {\n\t\terr = json.Unmarshal([]byte(d.config.WebhookData), &webhookData)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to unmarshal webhook data: %w\", err)\n\t\t}\n\n\t\tif webhookMethod == http.MethodGet || webhookContentType == CONTENT_TYPE_FORM || webhookContentType == CONTENT_TYPE_MULTIPART {\n\t\t\ttemp := make(map[string]string)\n\t\t\tjsonb, err := json.Marshal(webhookData)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to unmarshal webhook data: %w\", err)\n\t\t\t} else if err := json.Unmarshal(jsonb, &temp); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to unmarshal webhook data: %w\", err)\n\t\t\t} else {\n\t\t\t\twebhookData = temp\n\t\t\t}\n\t\t}\n\t}\n\n\t// 替换变量值\n\twebhookUrl.Path = strings.ReplaceAll(webhookUrl.Path, \"${CERTIMATE_DEPLOYER_COMMONNAME}\", url.PathEscape(xcertx509.GetSubjectCommonName(certX509)))\n\treplaceJsonValueRecursively(webhookData, \"${CERTIMATE_DEPLOYER_COMMONNAME}\", xcertx509.GetSubjectCommonName(certX509))\n\treplaceJsonValueRecursively(webhookData, \"${CERTIMATE_DEPLOYER_SUBJECTALTNAMES}\", strings.Join(xcertx509.GetSubjectAltNames(certX509), \";\"))\n\treplaceJsonValueRecursively(webhookData, \"${CERTIMATE_DEPLOYER_CERTIFICATE}\", certPEM)\n\treplaceJsonValueRecursively(webhookData, \"${CERTIMATE_DEPLOYER_CERTIFICATE_SERVER}\", serverCertPEM)\n\treplaceJsonValueRecursively(webhookData, \"${CERTIMATE_DEPLOYER_CERTIFICATE_INTERMEDIA}\", intermediaCertPEM)\n\treplaceJsonValueRecursively(webhookData, \"${CERTIMATE_DEPLOYER_PRIVATEKEY}\", privkeyPEM)\n\n\t// 兼容旧版变量\n\twebhookUrl.Path = strings.ReplaceAll(webhookUrl.Path, \"${DOMAIN}\", url.PathEscape(certX509.Subject.CommonName))\n\treplaceJsonValueRecursively(webhookData, \"${DOMAIN}\", certX509.Subject.CommonName)\n\treplaceJsonValueRecursively(webhookData, \"${DOMAINS}\", strings.Join(certX509.DNSNames, \";\"))\n\treplaceJsonValueRecursively(webhookData, \"${CERTIFICATE}\", certPEM)\n\treplaceJsonValueRecursively(webhookData, \"${SERVER_CERTIFICATE}\", serverCertPEM)\n\treplaceJsonValueRecursively(webhookData, \"${INTERMEDIA_CERTIFICATE}\", intermediaCertPEM)\n\treplaceJsonValueRecursively(webhookData, \"${PRIVATE_KEY}\", privkeyPEM)\n\n\t// 生成请求\n\t// 其中 GET 请求需转换为查询参数\n\treq := d.httpClient.R().SetHeaderMultiValues(webhookHeaders)\n\treq.URL = webhookUrl.String()\n\treq.Method = webhookMethod\n\tif webhookMethod == http.MethodGet {\n\t\treq.SetQueryParams(webhookData.(map[string]string))\n\t} else {\n\t\tswitch webhookContentType {\n\t\tcase CONTENT_TYPE_JSON:\n\t\t\treq.SetBody(webhookData)\n\t\tcase CONTENT_TYPE_FORM:\n\t\t\treq.SetFormData(webhookData.(map[string]string))\n\t\tcase CONTENT_TYPE_MULTIPART:\n\t\t\treq.SetMultipartFormData(webhookData.(map[string]string))\n\t\t}\n\t}\n\n\t// 发送请求\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to send webhook request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn nil, fmt.Errorf(\"unexpected webhook response status code: %d\", resp.StatusCode())\n\t}\n\n\td.logger.Debug(\"webhook responded\", slog.Any(\"response\", resp.String()))\n\n\treturn &deployer.DeployResult{}, nil\n}\n\nfunc replaceJsonValueRecursively(data interface{}, oldStr, newStr string) interface{} {\n\tswitch v := data.(type) {\n\tcase map[string]any:\n\t\tfor k, val := range v {\n\t\t\tv[k] = replaceJsonValueRecursively(val, oldStr, newStr)\n\t\t}\n\tcase []any:\n\t\tfor i, val := range v {\n\t\t\tv[i] = replaceJsonValueRecursively(val, oldStr, newStr)\n\t\t}\n\tcase string:\n\t\treturn strings.ReplaceAll(v, oldStr, newStr)\n\t}\n\treturn data\n}\n"
  },
  {
    "path": "pkg/core/deployer/providers/webhook/webhook_test.go",
    "content": "package webhook_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/deployer/providers/webhook\"\n)\n\nvar (\n\tfInputCertPath      string\n\tfInputKeyPath       string\n\tfWebhookUrl         string\n\tfWebhookContentType string\n\tfWebhookData        string\n)\n\nfunc init() {\n\targsPrefix := \"WEBHOOK_\"\n\n\tflag.StringVar(&fInputCertPath, argsPrefix+\"INPUTCERTPATH\", \"\", \"\")\n\tflag.StringVar(&fInputKeyPath, argsPrefix+\"INPUTKEYPATH\", \"\", \"\")\n\tflag.StringVar(&fWebhookUrl, argsPrefix+\"URL\", \"\", \"\")\n\tflag.StringVar(&fWebhookContentType, argsPrefix+\"CONTENTTYPE\", \"application/json\", \"\")\n\tflag.StringVar(&fWebhookData, argsPrefix+\"DATA\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./webhook_test.go -args \\\n\t--WEBHOOK_INPUTCERTPATH=\"/path/to/your-input-cert.pem\" \\\n\t--WEBHOOK_INPUTKEYPATH=\"/path/to/your-input-key.pem\" \\\n\t--WEBHOOK_URL=\"https://example.com/your-webhook-url\" \\\n\t--WEBHOOK_CONTENTTYPE=\"application/json\" \\\n\t--WEBHOOK_DATA=\"{\\\"certificate\\\":\\\"${CERTIFICATE}\\\",\\\"privateKey\\\":\\\"${PRIVATE_KEY}\\\"}\"\n*/\nfunc TestDeploy(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Deploy\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"INPUTCERTPATH: %v\", fInputCertPath),\n\t\t\tfmt.Sprintf(\"INPUTKEYPATH: %v\", fInputKeyPath),\n\t\t\tfmt.Sprintf(\"WEBHOOKURL: %v\", fWebhookUrl),\n\t\t\tfmt.Sprintf(\"WEBHOOKCONTENTTYPE: %v\", fWebhookContentType),\n\t\t\tfmt.Sprintf(\"WEBHOOKDATA: %v\", fWebhookData),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewDeployer(&provider.DeployerConfig{\n\t\t\tWebhookUrl:  fWebhookUrl,\n\t\t\tWebhookData: fWebhookData,\n\t\t\tMethod:      \"POST\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Content-Type\": fWebhookContentType,\n\t\t\t},\n\t\t\tAllowInsecureConnections: true,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfInputCertData, _ := os.ReadFile(fInputCertPath)\n\t\tfInputKeyData, _ := os.ReadFile(fInputKeyPath)\n\t\tres, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/notifier/provider.go",
    "content": "package notifier\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n)\n\n// 表示定义消息通知器的抽象类型接口。\ntype Provider interface {\n\t// 设置日志记录器。\n\t//\n\t// 入参：\n\t//   - logger：日志记录器实例。\n\tSetLogger(logger *slog.Logger)\n\n\t// 发送通知。\n\t//\n\t// 入参：\n\t//   - ctx：上下文。\n\t//   - subject：通知主题。\n\t//   - message：通知内容。\n\t//\n\t// 出参：\n\t//   - res：发送结果。\n\t//   - err: 错误。\n\tNotify(ctx context.Context, subject, message string) (_res *NotifyResult, _err error)\n}\n\n// 表示通知发送结果的数据结构。\ntype NotifyResult struct {\n\tExtendedData map[string]any `json:\"extendedData,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go",
    "content": "package dingtalkbot\n\nimport (\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n)\n\ntype NotifierConfig struct {\n\t// 钉钉机器人的 Webhook 地址。\n\tWebhookUrl string `json:\"webhookUrl\"`\n\t// 钉钉机器人的 Secret。\n\tSecret string `json:\"secret\"`\n\t// 自定义消息数据。\n\t// 选填。\n\tCustomPayload string `json:\"customPayload,omitempty\"`\n}\n\ntype Notifier struct {\n\tconfig     *NotifierConfig\n\tlogger     *slog.Logger\n\thttpClient *resty.Client\n}\n\nvar _ notifier.Provider = (*Notifier)(nil)\n\nfunc NewNotifier(config *NotifierConfig) (*Notifier, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the notifier provider is nil\")\n\t}\n\n\tclient := resty.New().\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\tif config.Secret != \"\" {\n\t\t\t\ttimestamp := fmt.Sprintf(\"%d\", time.Now().UnixMilli())\n\n\t\t\t\th := hmac.New(sha256.New, []byte(config.Secret))\n\t\t\t\th.Write([]byte(fmt.Sprintf(\"%s\\n%s\", timestamp, config.Secret)))\n\t\t\t\tsign := base64.StdEncoding.EncodeToString(h.Sum(nil))\n\n\t\t\t\tqs := req.URL.Query()\n\t\t\t\tqs.Set(\"timestamp\", timestamp)\n\t\t\t\tqs.Set(\"sign\", sign)\n\t\t\t\treq.URL.RawQuery = qs.Encode()\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\treturn &Notifier{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\thttpClient: client,\n\t}, nil\n}\n\nfunc (n *Notifier) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tn.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tn.logger = logger\n\t}\n}\n\nfunc (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) {\n\twebhookUrl, err := url.Parse(n.config.WebhookUrl)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dingtalk api error: invalid webhook url: %w\", err)\n\t} else {\n\t\tconst hostname = \"oapi.dingtalk.com\"\n\t\tif webhookUrl.Hostname() != hostname {\n\t\t\tn.logger.Warn(fmt.Sprintf(\"the webhook url hostname is not '%s', please make sure it is correct\", hostname))\n\t\t}\n\t}\n\n\tvar webhookData map[string]any\n\tif n.config.CustomPayload == \"\" {\n\t\twebhookData = map[string]any{\n\t\t\t\"msgtype\": \"text\",\n\t\t\t\"text\": map[string]string{\n\t\t\t\t\"content\": subject + \"\\n\\n\" + message,\n\t\t\t},\n\t\t}\n\t} else {\n\t\terr = json.Unmarshal([]byte(n.config.CustomPayload), &webhookData)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to unmarshal webhook data: %w\", err)\n\t\t}\n\n\t\treplaceJsonValueRecursively(webhookData, \"${CERTIMATE_NOTIFIER_SUBJECT}\", subject)\n\t\treplaceJsonValueRecursively(webhookData, \"${CERTIMATE_NOTIFIER_MESSAGE}\", message)\n\n\t\treplaceJsonValueRecursively(webhookData, \"${SUBJECT}\", subject)\n\t\treplaceJsonValueRecursively(webhookData, \"${MESSAGE}\", message)\n\t}\n\n\t// REF: https://open.dingtalk.com/document/development/custom-robots-send-group-messages\n\tvar result struct {\n\t\tErrorCode    int    `json:\"errcode\"`\n\t\tErrorMessage string `json:\"errmsg\"`\n\t}\n\treq := n.httpClient.R().\n\t\tSetContext(ctx).\n\t\tSetBody(webhookData)\n\tresp, err := req.Post(webhookUrl.String())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dingtalk api error: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn nil, fmt.Errorf(\"dingtalk api error: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t} else if err := json.Unmarshal(resp.Body(), &result); err != nil {\n\t\treturn nil, fmt.Errorf(\"dingtalk api error: %w (resp: %s)\", err, resp.String())\n\t} else if result.ErrorCode != 0 {\n\t\treturn nil, fmt.Errorf(\"dingtalk api error: errcode='%d', errmsg='%s'\", result.ErrorCode, result.ErrorMessage)\n\t}\n\n\treturn &notifier.NotifyResult{}, nil\n}\n\nfunc replaceJsonValueRecursively(data interface{}, oldStr, newStr string) interface{} {\n\tswitch v := data.(type) {\n\tcase map[string]any:\n\t\tfor k, val := range v {\n\t\t\tv[k] = replaceJsonValueRecursively(val, oldStr, newStr)\n\t\t}\n\tcase []any:\n\t\tfor i, val := range v {\n\t\t\tv[i] = replaceJsonValueRecursively(val, oldStr, newStr)\n\t\t}\n\tcase []string:\n\t\tfor i, s := range v {\n\t\t\tvar val interface{} = s\n\t\t\tvar newVal interface{} = replaceJsonValueRecursively(val, oldStr, newStr)\n\t\t\tv[i] = newVal.(string)\n\t\t}\n\tcase string:\n\t\treturn strings.ReplaceAll(v, oldStr, newStr)\n\t}\n\treturn data\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/dingtalkbot/dingtalkbot_test.go",
    "content": "package dingtalkbot_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/notifier/providers/dingtalkbot\"\n)\n\nconst (\n\tmockSubject = \"test_subject\"\n\tmockMessage = \"test_message\"\n)\n\nvar (\n\tfWebhookUrl string\n\tfSecret     string\n)\n\nfunc init() {\n\targsPrefix := \"DINGTALKBOT_\"\n\n\tflag.StringVar(&fWebhookUrl, argsPrefix+\"WEBHOOKURL\", \"\", \"\")\n\tflag.StringVar(&fSecret, argsPrefix+\"SECRET\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./dingtalkbot_test.go -args \\\n\t--DINGTALKBOT_WEBHOOKURL=\"https://example.com/your-webhook-url\" \\\n\t--DINGTALKBOT_SECRET=\"your-secret\"\n*/\nfunc TestNotify(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Notify\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"WEBHOOKURL: %v\", fWebhookUrl),\n\t\t\tfmt.Sprintf(\"SECRET: %v\", fSecret),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewNotifier(&provider.NotifierConfig{\n\t\t\tWebhookUrl: fWebhookUrl,\n\t\t\tSecret:     fSecret,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tres, err := provider.Notify(context.Background(), mockSubject, mockMessage)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/discordbot/discordbot.go",
    "content": "package discordbot\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n)\n\ntype NotifierConfig struct {\n\t// Discord Bot API Token。\n\tBotToken string `json:\"botToken\"`\n\t// Discord Channel ID。\n\tChannelId string `json:\"channelId\"`\n}\n\ntype Notifier struct {\n\tconfig     *NotifierConfig\n\tlogger     *slog.Logger\n\thttpClient *resty.Client\n}\n\nvar _ notifier.Provider = (*Notifier)(nil)\n\nfunc NewNotifier(config *NotifierConfig) (*Notifier, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the notifier provider is nil\")\n\t}\n\n\tclient := resty.New().\n\t\tSetHeader(\"Authorization\", fmt.Sprintf(\"Bot %s\", config.BotToken)).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent)\n\n\treturn &Notifier{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\thttpClient: client,\n\t}, nil\n}\n\nfunc (n *Notifier) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tn.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tn.logger = logger\n\t}\n}\n\nfunc (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) {\n\t// REF: https://discord.com/developers/docs/resources/message#create-message\n\treq := n.httpClient.R().\n\t\tSetContext(ctx).\n\t\tSetBody(map[string]any{\n\t\t\t\"content\": subject + \"\\n\" + message,\n\t\t})\n\tresp, err := req.Post(fmt.Sprintf(\"https://discord.com/api/v9/channels/%s/messages\", n.config.ChannelId))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"discord api error: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn nil, fmt.Errorf(\"discord api error: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn &notifier.NotifyResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/discordbot/discordbot_test.go",
    "content": "package discordbot_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/notifier/providers/discordbot\"\n)\n\nconst (\n\tmockSubject = \"test_subject\"\n\tmockMessage = \"test_message\"\n)\n\nvar (\n\tfApiToken  string\n\tfChannelId string\n)\n\nfunc init() {\n\targsPrefix := \"DISCORDBOT_\"\n\n\tflag.StringVar(&fApiToken, argsPrefix+\"APITOKEN\", \"\", \"\")\n\tflag.StringVar(&fChannelId, argsPrefix+\"CHANNELID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./discordbot_test.go -args \\\n\t--DISCORDBOT_APITOKEN=\"your-bot-token\" \\\n\t--DISCORDBOT_CHANNELID=\"your-channel-id\"\n*/\nfunc TestNotify(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Notify\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"APITOKEN: %v\", fApiToken),\n\t\t\tfmt.Sprintf(\"CHANNELID: %v\", fChannelId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewNotifier(&provider.NotifierConfig{\n\t\t\tBotToken:  fApiToken,\n\t\t\tChannelId: fChannelId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tres, err := provider.Notify(context.Background(), mockSubject, mockMessage)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/email/consts.go",
    "content": "package email\n\nconst (\n\tMESSAGE_FORMAT_PLAIN = \"plain\"\n\tMESSAGE_FORMAT_HTML  = \"html\"\n)\n"
  },
  {
    "path": "pkg/core/notifier/providers/email/email.go",
    "content": "package email\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/microcosm-cc/bluemonday\"\n\n\t\"github.com/certimate-go/certimate/internal/tools/smtp\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n)\n\ntype NotifierConfig struct {\n\t// SMTP 服务器地址。\n\tSmtpHost string `json:\"smtpHost\"`\n\t// SMTP 服务器端口。\n\t// 零值时根据是否启用 TLS 决定。\n\tSmtpPort int32 `json:\"smtpPort\"`\n\t// 是否启用 TLS。\n\tSmtpTls bool `json:\"smtpTls\"`\n\t// 用户名。\n\tUsername string `json:\"username\"`\n\t// 密码。\n\tPassword string `json:\"password\"`\n\t// 发件人邮箱。\n\tSenderAddress string `json:\"senderAddress\"`\n\t// 发件人显示名称。\n\tSenderName string `json:\"senderName,omitempty\"`\n\t// 收件人邮箱。\n\tReceiverAddress string `json:\"receiverAddress\"`\n\t// 消息格式。\n\t// 可取值 [MESSAGE_FORMAT_PLAIN]、[MESSAGE_FORMAT_HTML]。\n\t// 零值时默认值 [MESSAGE_FORMAT_PLAIN]。\n\tMessageFormat string `json:\"messageFormat,omitempty\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype Notifier struct {\n\tconfig *NotifierConfig\n\tlogger *slog.Logger\n}\n\nvar _ notifier.Provider = (*Notifier)(nil)\n\nfunc NewNotifier(config *NotifierConfig) (*Notifier, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the notifier provider is nil\")\n\t}\n\n\treturn &Notifier{\n\t\tconfig: config,\n\t\tlogger: slog.Default(),\n\t}, nil\n}\n\nfunc (n *Notifier) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tn.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tn.logger = logger\n\t}\n}\n\nfunc (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) {\n\tclientCfg := smtp.NewDefaultConfig()\n\tclientCfg.Host = n.config.SmtpHost\n\tclientCfg.Port = int(n.config.SmtpPort)\n\tclientCfg.Username = n.config.Username\n\tclientCfg.Password = n.config.Password\n\tclientCfg.UseSsl = n.config.SmtpTls\n\tclientCfg.SkipTlsVerify = n.config.AllowInsecureConnections\n\tclient, err := smtp.NewClient(clientCfg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create SMTP client: %w\", err)\n\t}\n\n\tdefer client.Close()\n\n\tmsg := smtp.NewMessage()\n\tmsg.Subject(subject)\n\tswitch n.config.MessageFormat {\n\tcase \"\", MESSAGE_FORMAT_PLAIN:\n\t\tmsg.SetBodyString(smtp.MIMETypeTextPlain, message)\n\tcase MESSAGE_FORMAT_HTML:\n\t\tmsg.SetBodyString(smtp.MIMETypeTextHTML, bluemonday.UGCPolicy().Sanitize(message))\n\t\tmsg.AddAlternativeString(smtp.MIMETypeTextPlain, bluemonday.StrictPolicy().Sanitize(message))\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported message format: '%s'\", n.config.MessageFormat)\n\t}\n\n\tif n.config.SenderName == \"\" {\n\t\tmsg.From(n.config.SenderAddress)\n\t} else {\n\t\tmsg.FromFormat(n.config.SenderName, n.config.SenderAddress)\n\t}\n\tmsg.To(n.config.ReceiverAddress)\n\n\tif err := client.Send(ctx, msg); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to send mail: %w\", err)\n\t}\n\n\treturn &notifier.NotifyResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/email/email_test.go",
    "content": "package email_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/notifier/providers/email\"\n)\n\nconst (\n\tmockSubject     = \"test_subject\"\n\tmockMessage     = \"test_message\"\n\tmockHtmlMessage = \"<h1>Hello Certimate！</h1><a onblur=\\\"alert(secret)\\\" href=\\\"http://www.google.com\\\">Google</a>\"\n)\n\nvar (\n\tfSmtpHost        string\n\tfSmtpPort        int64\n\tfSmtpTLS         bool\n\tfUsername        string\n\tfPassword        string\n\tfSenderAddress   string\n\tfReceiverAddress string\n)\n\nfunc init() {\n\targsPrefix := \"EMAIL_\"\n\n\tflag.StringVar(&fSmtpHost, argsPrefix+\"SMTPHOST\", \"\", \"\")\n\tflag.Int64Var(&fSmtpPort, argsPrefix+\"SMTPPORT\", 0, \"\")\n\tflag.BoolVar(&fSmtpTLS, argsPrefix+\"SMTPTLS\", false, \"\")\n\tflag.StringVar(&fUsername, argsPrefix+\"USERNAME\", \"\", \"\")\n\tflag.StringVar(&fPassword, argsPrefix+\"PASSWORD\", \"\", \"\")\n\tflag.StringVar(&fSenderAddress, argsPrefix+\"SENDERADDRESS\", \"\", \"\")\n\tflag.StringVar(&fReceiverAddress, argsPrefix+\"RECEIVERADDRESS\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./email_test.go -args \\\n\t--EMAIL_SMTPHOST=\"smtp.example.com\" \\\n\t--EMAIL_SMTPPORT=465 \\\n\t--EMAIL_SMTPTLS=true \\\n\t--EMAIL_USERNAME=\"your-username\" \\\n\t--EMAIL_PASSWORD=\"your-password\" \\\n\t--EMAIL_SENDERADDRESS=\"sender@example.com\" \\\n\t--EMAIL_RECEIVERADDRESS=\"receiver@example.com\"\n*/\nfunc TestNotify(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Notify\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"SMTPHOST: %v\", fSmtpHost),\n\t\t\tfmt.Sprintf(\"SMTPPORT: %v\", fSmtpPort),\n\t\t\tfmt.Sprintf(\"SMTPTLS: %v\", fSmtpTLS),\n\t\t\tfmt.Sprintf(\"USERNAME: %v\", fUsername),\n\t\t\tfmt.Sprintf(\"PASSWORD: %v\", fPassword),\n\t\t\tfmt.Sprintf(\"SENDERADDRESS: %v\", fSenderAddress),\n\t\t\tfmt.Sprintf(\"RECEIVERADDRESS: %v\", fReceiverAddress),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewNotifier(&provider.NotifierConfig{\n\t\t\tSmtpHost:        fSmtpHost,\n\t\t\tSmtpPort:        int32(fSmtpPort),\n\t\t\tSmtpTls:         fSmtpTLS,\n\t\t\tUsername:        fUsername,\n\t\t\tPassword:        fPassword,\n\t\t\tSenderAddress:   fSenderAddress,\n\t\t\tReceiverAddress: fReceiverAddress,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tres, err := provider.Notify(context.Background(), mockSubject, mockMessage)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n\n\tt.Run(\"Notify_Html\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"SMTPHOST: %v\", fSmtpHost),\n\t\t\tfmt.Sprintf(\"SMTPPORT: %v\", fSmtpPort),\n\t\t\tfmt.Sprintf(\"SMTPTLS: %v\", fSmtpTLS),\n\t\t\tfmt.Sprintf(\"USERNAME: %v\", fUsername),\n\t\t\tfmt.Sprintf(\"PASSWORD: %v\", fPassword),\n\t\t\tfmt.Sprintf(\"SENDERADDRESS: %v\", fSenderAddress),\n\t\t\tfmt.Sprintf(\"RECEIVERADDRESS: %v\", fReceiverAddress),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewNotifier(&provider.NotifierConfig{\n\t\t\tSmtpHost:        fSmtpHost,\n\t\t\tSmtpPort:        int32(fSmtpPort),\n\t\t\tSmtpTls:         fSmtpTLS,\n\t\t\tUsername:        fUsername,\n\t\t\tPassword:        fPassword,\n\t\t\tSenderAddress:   fSenderAddress,\n\t\t\tReceiverAddress: fReceiverAddress,\n\t\t\tMessageFormat:   provider.MESSAGE_FORMAT_HTML,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tres, err := provider.Notify(context.Background(), mockSubject, mockHtmlMessage)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/larkbot/larkbot.go",
    "content": "package larkbot\n\nimport (\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n)\n\ntype NotifierConfig struct {\n\t// 飞书机器人 Webhook 地址。\n\tWebhookUrl string `json:\"webhookUrl\"`\n\t// 飞书机器人的 Secret。\n\tSecret string `json:\"secret\"`\n\t// 自定义消息数据。\n\t// 选填。\n\tCustomPayload string `json:\"customPayload,omitempty\"`\n}\n\ntype Notifier struct {\n\tconfig     *NotifierConfig\n\tlogger     *slog.Logger\n\thttpClient *resty.Client\n}\n\nvar _ notifier.Provider = (*Notifier)(nil)\n\nfunc NewNotifier(config *NotifierConfig) (*Notifier, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the notifier provider is nil\")\n\t}\n\n\tclient := resty.New().\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent)\n\n\treturn &Notifier{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\thttpClient: client,\n\t}, nil\n}\n\nfunc (n *Notifier) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tn.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tn.logger = logger\n\t}\n}\n\nfunc (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) {\n\twebhookUrl, err := url.Parse(n.config.WebhookUrl)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"lark api error: invalid webhook url: %w\", err)\n\t} else {\n\t\tconst hostname = \"open.larksuite.com\"\n\t\tconst hostname_cn = \"open.feishu.cn\"\n\t\tif webhookUrl.Hostname() != hostname && webhookUrl.Hostname() != hostname_cn {\n\t\t\tn.logger.Warn(fmt.Sprintf(\"the webhook url hostname is not '%s' or '%s', please make sure it is correct\", hostname, hostname_cn))\n\t\t}\n\t}\n\n\tvar webhookData map[string]any\n\tif n.config.CustomPayload == \"\" {\n\t\twebhookData = map[string]any{\n\t\t\t\"msg_type\": \"text\",\n\t\t\t\"content\": map[string]string{\n\t\t\t\t\"text\": subject + \"\\n\\n\" + message,\n\t\t\t},\n\t\t}\n\t} else {\n\t\terr = json.Unmarshal([]byte(n.config.CustomPayload), &webhookData)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to unmarshal webhook data: %w\", err)\n\t\t}\n\n\t\treplaceJsonValueRecursively(webhookData, \"${CERTIMATE_NOTIFIER_SUBJECT}\", subject)\n\t\treplaceJsonValueRecursively(webhookData, \"${CERTIMATE_NOTIFIER_MESSAGE}\", message)\n\n\t\treplaceJsonValueRecursively(webhookData, \"${SUBJECT}\", subject)\n\t\treplaceJsonValueRecursively(webhookData, \"${MESSAGE}\", message)\n\t}\n\n\tif n.config.Secret != \"\" {\n\t\ttimestamp := fmt.Sprintf(\"%d\", time.Now().Unix())\n\n\t\tstringToSign := fmt.Sprintf(\"%s\\n%s\", timestamp, n.config.Secret)\n\n\t\th := hmac.New(sha256.New, []byte(stringToSign))\n\t\tvar data []byte\n\t\t_, err := h.Write(data)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"lark api error: failed to calc sign: %w\", err)\n\t\t}\n\t\tsign := base64.StdEncoding.EncodeToString(h.Sum(nil))\n\n\t\twebhookData[\"timestamp\"] = timestamp\n\t\twebhookData[\"sign\"] = sign\n\t}\n\n\t// REF: https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot\n\t// REF: https://open.larksuite.com/document/client-docs/bot-v3/add-custom-bot\n\tvar result struct {\n\t\tCode    int    `json:\"code\"`\n\t\tMessage string `json:\"msg\"`\n\t}\n\treq := n.httpClient.R().\n\t\tSetContext(ctx).\n\t\tSetBody(webhookData)\n\tresp, err := req.Post(webhookUrl.String())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"lark api error: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn nil, fmt.Errorf(\"lark api error: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t} else if err := json.Unmarshal(resp.Body(), &result); err != nil {\n\t\treturn nil, fmt.Errorf(\"lark api error: %w (resp: %s)\", err, resp.String())\n\t} else if result.Code != 0 {\n\t\treturn nil, fmt.Errorf(\"lark api error: code='%d', msg='%s'\", result.Code, result.Message)\n\t}\n\n\treturn &notifier.NotifyResult{}, nil\n}\n\nfunc replaceJsonValueRecursively(data interface{}, oldStr, newStr string) interface{} {\n\tswitch v := data.(type) {\n\tcase map[string]any:\n\t\tfor k, val := range v {\n\t\t\tv[k] = replaceJsonValueRecursively(val, oldStr, newStr)\n\t\t}\n\tcase []any:\n\t\tfor i, val := range v {\n\t\t\tv[i] = replaceJsonValueRecursively(val, oldStr, newStr)\n\t\t}\n\tcase []string:\n\t\tfor i, s := range v {\n\t\t\tvar val interface{} = s\n\t\t\tvar newVal interface{} = replaceJsonValueRecursively(val, oldStr, newStr)\n\t\t\tv[i] = newVal.(string)\n\t\t}\n\tcase string:\n\t\treturn strings.ReplaceAll(v, oldStr, newStr)\n\t}\n\treturn data\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/larkbot/larkbot_test.go",
    "content": "package larkbot_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/notifier/providers/larkbot\"\n)\n\nconst (\n\tmockSubject = \"test_subject\"\n\tmockMessage = \"test_message\"\n)\n\nvar (\n\tfWebhookUrl string\n\tfSecret     string\n)\n\nfunc init() {\n\targsPrefix := \"LARKBOT_\"\n\n\tflag.StringVar(&fWebhookUrl, argsPrefix+\"WEBHOOKURL\", \"\", \"\")\n\tflag.StringVar(&fSecret, argsPrefix+\"SECRET\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./larkbot_test.go -args \\\n\t--LARKBOT_WEBHOOKURL=\"https://example.com/your-webhook-url\" \\\n\t--LARKBOT_SECRET=\"your-secret\"\n*/\nfunc TestNotify(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Notify\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"WEBHOOKURL: %v\", fWebhookUrl),\n\t\t\tfmt.Sprintf(\"SECRET: %v\", fSecret),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewNotifier(&provider.NotifierConfig{\n\t\t\tWebhookUrl: fWebhookUrl,\n\t\t\tSecret:     fSecret,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tres, err := provider.Notify(context.Background(), mockSubject, mockMessage)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/mattermost/mattermost.go",
    "content": "package mattermost\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n)\n\ntype NotifierConfig struct {\n\t// Mattermost 服务地址。\n\tServerUrl string `json:\"serverUrl\"`\n\t// Mattermost 用户名。\n\tUsername string `json:\"username\"`\n\t// Mattermost 密码。\n\tPassword string `json:\"password\"`\n\t// Mattermost 频道 ID。\n\tChannelId string `json:\"channelId\"`\n}\n\ntype Notifier struct {\n\tconfig     *NotifierConfig\n\tlogger     *slog.Logger\n\thttpClient *resty.Client\n}\n\nvar _ notifier.Provider = (*Notifier)(nil)\n\nfunc NewNotifier(config *NotifierConfig) (*Notifier, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the notifier provider is nil\")\n\t}\n\n\tclient := resty.New().\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent)\n\n\treturn &Notifier{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\thttpClient: client,\n\t}, nil\n}\n\nfunc (n *Notifier) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tn.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tn.logger = logger\n\t}\n}\n\nfunc (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) {\n\tserverUrl := strings.TrimRight(n.config.ServerUrl, \"/\")\n\n\t// REF: https://developers.mattermost.com/api-documentation/#/operations/Login\n\tloginReq := n.httpClient.R().\n\t\tSetContext(ctx).\n\t\tSetBody(map[string]any{\n\t\t\t\"login_id\": n.config.Username,\n\t\t\t\"password\": n.config.Password,\n\t\t})\n\tloginResp, err := loginReq.Post(fmt.Sprintf(\"%s/api/v4/users/login\", serverUrl))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mattermost api error: failed to send request: %w\", err)\n\t} else if loginResp.IsError() {\n\t\treturn nil, fmt.Errorf(\"mattermost api error: unexpected status code: %d (resp: %s)\", loginResp.StatusCode(), loginResp.String())\n\t} else if loginResp.Header().Get(\"Token\") == \"\" {\n\t\treturn nil, fmt.Errorf(\"mattermost api error: received empty login token\")\n\t}\n\n\t// REF: https://developers.mattermost.com/api-documentation/#/operations/CreatePost\n\tpostReq := n.httpClient.R().\n\t\tSetContext(ctx).\n\t\tSetHeader(\"Authorization\", \"Bearer \"+loginResp.Header().Get(\"Token\")).\n\t\tSetBody(map[string]any{\n\t\t\t\"channel_id\": n.config.ChannelId,\n\t\t\t\"props\": map[string]interface{}{\n\t\t\t\t\"attachments\": []map[string]interface{}{\n\t\t\t\t\t{\n\t\t\t\t\t\t\"title\": subject,\n\t\t\t\t\t\t\"text\":  message,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tpostResp, err := postReq.Post(fmt.Sprintf(\"%s/api/v4/posts\", serverUrl))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mattermost api error: failed to send request: %w\", err)\n\t} else if postResp.IsError() {\n\t\treturn nil, fmt.Errorf(\"mattermost api error: unexpected status code: %d (resp: %s)\", postResp.StatusCode(), postResp.String())\n\t}\n\n\treturn &notifier.NotifyResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/mattermost/mattermost_test.go",
    "content": "package mattermost_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/notifier/providers/mattermost\"\n)\n\nconst (\n\tmockSubject = \"test_subject\"\n\tmockMessage = \"test_message\"\n)\n\nvar (\n\tfServerUrl string\n\tfChannelId string\n\tfUsername  string\n\tfPassword  string\n)\n\nfunc init() {\n\targsPrefix := \"MATTERMOST_\"\n\n\tflag.StringVar(&fServerUrl, argsPrefix+\"SERVERURL\", \"\", \"\")\n\tflag.StringVar(&fChannelId, argsPrefix+\"CHANNELID\", \"\", \"\")\n\tflag.StringVar(&fUsername, argsPrefix+\"USERNAME\", \"\", \"\")\n\tflag.StringVar(&fPassword, argsPrefix+\"PASSWORD\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./mattermost_test.go -args \\\n\t--MATTERMOST_SERVERURL=\"https://example.com/your-server-url\" \\\n\t--MATTERMOST_CHANNELID=\"your-chanel-id\" \\\n\t--MATTERMOST_USERNAME=\"your-username\" \\\n\t--MATTERMOST_PASSWORD=\"your-password\"\n*/\nfunc TestNotify(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Notify\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"SERVERURL: %v\", fServerUrl),\n\t\t\tfmt.Sprintf(\"CHANNELID: %v\", fChannelId),\n\t\t\tfmt.Sprintf(\"USERNAME: %v\", fUsername),\n\t\t\tfmt.Sprintf(\"PASSWORD: %v\", fPassword),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewNotifier(&provider.NotifierConfig{\n\t\t\tServerUrl: fServerUrl,\n\t\t\tChannelId: fChannelId,\n\t\t\tUsername:  fUsername,\n\t\t\tPassword:  fPassword,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tres, err := provider.Notify(context.Background(), mockSubject, mockMessage)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/slackbot/slackbot.go",
    "content": "package discordbot\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n)\n\ntype NotifierConfig struct {\n\t// Slack Bot API Token。\n\tBotToken string `json:\"botToken\"`\n\t// Slack Channel ID。\n\tChannelId string `json:\"channelId\"`\n}\n\ntype Notifier struct {\n\tconfig     *NotifierConfig\n\tlogger     *slog.Logger\n\thttpClient *resty.Client\n}\n\nvar _ notifier.Provider = (*Notifier)(nil)\n\nfunc NewNotifier(config *NotifierConfig) (*Notifier, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the notifier provider is nil\")\n\t}\n\n\tclient := resty.New().\n\t\tSetHeader(\"Authorization\", fmt.Sprintf(\"Bearer %s\", config.BotToken)).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent)\n\n\treturn &Notifier{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\thttpClient: client,\n\t}, nil\n}\n\nfunc (n *Notifier) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tn.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tn.logger = logger\n\t}\n}\n\nfunc (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) {\n\t// REF: https://docs.slack.dev/messaging/sending-and-scheduling-messages#publishing\n\treq := n.httpClient.R().\n\t\tSetContext(ctx).\n\t\tSetBody(map[string]any{\n\t\t\t\"token\":   n.config.BotToken,\n\t\t\t\"channel\": n.config.ChannelId,\n\t\t\t\"text\":    subject + \"\\n\" + message,\n\t\t})\n\tresp, err := req.Post(\"https://slack.com/api/chat.postMessage\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"slack api error: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn nil, fmt.Errorf(\"slack api error: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn &notifier.NotifyResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/slackbot/slackbot_test.go",
    "content": "package discordbot_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/notifier/providers/slackbot\"\n)\n\nconst (\n\tmockSubject = \"test_subject\"\n\tmockMessage = \"test_message\"\n)\n\nvar (\n\tfApiToken  string\n\tfChannelId string\n)\n\nfunc init() {\n\targsPrefix := \"SLACKBOT_\"\n\n\tflag.StringVar(&fApiToken, argsPrefix+\"APITOKEN\", \"\", \"\")\n\tflag.StringVar(&fChannelId, argsPrefix+\"CHANNELID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./slackbot_test.go -args \\\n\t--SLACKBOT_APITOKEN=\"your-bot-token\" \\\n\t--SLACKBOT_CHANNELID=\"your-channel-id\"\n*/\nfunc TestNotify(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Notify\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"APITOKEN: %v\", fApiToken),\n\t\t\tfmt.Sprintf(\"CHANNELID: %v\", fChannelId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewNotifier(&provider.NotifierConfig{\n\t\t\tBotToken:  fApiToken,\n\t\t\tChannelId: fChannelId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tres, err := provider.Notify(context.Background(), mockSubject, mockMessage)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/telegrambot/telegrambot.go",
    "content": "package telegrambot\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n)\n\ntype NotifierConfig struct {\n\t// Telegram Bot API Token。\n\tBotToken string `json:\"botToken\"`\n\t// Telegram Chat ID。\n\tChatId string `json:\"chatId\"`\n}\n\ntype Notifier struct {\n\tconfig     *NotifierConfig\n\tlogger     *slog.Logger\n\thttpClient *resty.Client\n}\n\nvar _ notifier.Provider = (*Notifier)(nil)\n\nfunc NewNotifier(config *NotifierConfig) (*Notifier, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the notifier provider is nil\")\n\t}\n\n\tclient := resty.New().\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent)\n\n\treturn &Notifier{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\thttpClient: client,\n\t}, nil\n}\n\nfunc (n *Notifier) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tn.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tn.logger = logger\n\t}\n}\n\nfunc (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) {\n\t// REF: https://core.telegram.org/bots/api#sendmessage\n\treq := n.httpClient.R().\n\t\tSetContext(ctx).\n\t\tSetBody(map[string]any{\n\t\t\t\"chat_id\": n.config.ChatId,\n\t\t\t\"text\":    subject + \"\\n\" + message,\n\t\t})\n\tresp, err := req.Post(fmt.Sprintf(\"https://api.telegram.org/bot%s/sendMessage\", n.config.BotToken))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"telegram api error: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn nil, fmt.Errorf(\"telegram api error: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn &notifier.NotifyResult{}, nil\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/telegrambot/telegrambot_test.go",
    "content": "package telegrambot_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/notifier/providers/telegrambot\"\n)\n\nconst (\n\tmockSubject = \"test_subject\"\n\tmockMessage = \"test_message\"\n)\n\nvar (\n\tfApiToken string\n\tfChatId   string\n)\n\nfunc init() {\n\targsPrefix := \"TELEGRAMBOT_\"\n\n\tflag.StringVar(&fApiToken, argsPrefix+\"APITOKEN\", \"\", \"\")\n\tflag.StringVar(&fChatId, argsPrefix+\"CHATID\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./telegrambot_test.go -args \\\n\t--TELEGRAMBOT_APITOKEN=\"your-api-token\" \\\n\t--TELEGRAMBOT_CHATID=\"your-chat-id\"\n*/\nfunc TestNotify(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Notify\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"APITOKEN: %v\", fApiToken),\n\t\t\tfmt.Sprintf(\"CHATID: %v\", fChatId),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewNotifier(&provider.NotifierConfig{\n\t\t\tBotToken: fApiToken,\n\t\t\tChatId:   fChatId,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tres, err := provider.Notify(context.Background(), mockSubject, mockMessage)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/webhook/webhook.go",
    "content": "package webhook\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n)\n\ntype NotifierConfig struct {\n\t// Webhook URL。\n\tWebhookUrl string `json:\"webhookUrl\"`\n\t// Webhook 回调数据（application/json 或 application/x-www-form-urlencoded 格式）。\n\tWebhookData string `json:\"webhookData,omitempty\"`\n\t// 请求谓词。\n\t// 零值时默认值 \"POST\"。\n\tMethod string `json:\"method,omitempty\"`\n\t// 请求标头。\n\tHeaders map[string]string `json:\"headers,omitempty\"`\n\t// 请求超时（单位：秒）。\n\t// 零值时默认值 30。\n\tTimeout int `json:\"timeout,omitempty\"`\n\t// 是否允许不安全的连接。\n\tAllowInsecureConnections bool `json:\"allowInsecureConnections,omitempty\"`\n}\n\ntype Notifier struct {\n\tconfig     *NotifierConfig\n\tlogger     *slog.Logger\n\thttpClient *resty.Client\n}\n\nvar _ notifier.Provider = (*Notifier)(nil)\n\nfunc NewNotifier(config *NotifierConfig) (*Notifier, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the notifier provider is nil\")\n\t}\n\n\tclient := resty.New().\n\t\tSetTimeout(30 * time.Second).\n\t\tSetRetryCount(3).\n\t\tSetRetryWaitTime(5 * time.Second)\n\tif config.Timeout > 0 {\n\t\tclient.SetTimeout(time.Duration(config.Timeout) * time.Second)\n\t}\n\tif config.AllowInsecureConnections {\n\t\tclient.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})\n\t}\n\n\treturn &Notifier{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\thttpClient: client,\n\t}, nil\n}\n\nfunc (n *Notifier) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tn.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tn.logger = logger\n\t}\n}\n\nfunc (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) {\n\t// 处理 Webhook URL\n\twebhookUrl, err := url.Parse(n.config.WebhookUrl)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse webhook url: %w\", err)\n\t} else if webhookUrl.Scheme != \"http\" && webhookUrl.Scheme != \"https\" {\n\t\treturn nil, fmt.Errorf(\"unsupported webhook url scheme '%s'\", webhookUrl.Scheme)\n\t}\n\n\t// 处理 Webhook 请求谓词\n\twebhookMethod := strings.ToUpper(n.config.Method)\n\tif webhookMethod == \"\" {\n\t\twebhookMethod = http.MethodPost\n\t} else if webhookMethod != http.MethodGet &&\n\t\twebhookMethod != http.MethodPost &&\n\t\twebhookMethod != http.MethodPut &&\n\t\twebhookMethod != http.MethodPatch &&\n\t\twebhookMethod != http.MethodDelete {\n\t\treturn nil, fmt.Errorf(\"unsupported webhook request method '%s'\", webhookMethod)\n\t}\n\n\t// 处理 Webhook 请求标头\n\twebhookHeaders := make(http.Header)\n\tfor k, v := range n.config.Headers {\n\t\twebhookHeaders.Set(k, v)\n\t}\n\n\t// 处理 Webhook 请求内容类型\n\tconst CONTENT_TYPE_JSON = \"application/json\"\n\tconst CONTENT_TYPE_FORM = \"application/x-www-form-urlencoded\"\n\tconst CONTENT_TYPE_MULTIPART = \"multipart/form-data\"\n\twebhookContentType := webhookHeaders.Get(\"Content-Type\")\n\tif webhookContentType == \"\" {\n\t\twebhookContentType = CONTENT_TYPE_JSON\n\t\twebhookHeaders.Set(\"Content-Type\", CONTENT_TYPE_JSON)\n\t} else if strings.HasPrefix(webhookContentType, CONTENT_TYPE_JSON) &&\n\t\tstrings.HasPrefix(webhookContentType, CONTENT_TYPE_FORM) &&\n\t\tstrings.HasPrefix(webhookContentType, CONTENT_TYPE_MULTIPART) {\n\t\treturn nil, fmt.Errorf(\"unsupported webhook content type '%s'\", webhookContentType)\n\t}\n\n\t// 处理 Webhook 请求数据\n\tvar webhookData interface{}\n\tif n.config.WebhookData == \"\" {\n\t\twebhookData = map[string]string{\n\t\t\t\"subject\": subject,\n\t\t\t\"message\": message,\n\t\t}\n\t} else {\n\t\terr = json.Unmarshal([]byte(n.config.WebhookData), &webhookData)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to unmarshal webhook data: %w\", err)\n\t\t}\n\n\t\tif webhookMethod == http.MethodGet || webhookContentType == CONTENT_TYPE_FORM || webhookContentType == CONTENT_TYPE_MULTIPART {\n\t\t\ttemp := make(map[string]string)\n\t\t\tjsonb, err := json.Marshal(webhookData)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to unmarshal webhook data: %w\", err)\n\t\t\t} else if err := json.Unmarshal(jsonb, &temp); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to unmarshal webhook data: %w\", err)\n\t\t\t} else {\n\t\t\t\twebhookData = temp\n\t\t\t}\n\t\t}\n\t}\n\n\t// 替换变量值\n\treplaceJsonValueRecursively(webhookData, \"${CERTIMATE_NOTIFIER_SUBJECT}\", subject)\n\treplaceJsonValueRecursively(webhookData, \"${CERTIMATE_NOTIFIER_MESSAGE}\", message)\n\n\t// 兼容旧版变量\n\treplaceJsonValueRecursively(webhookData, \"${SUBJECT}\", subject)\n\treplaceJsonValueRecursively(webhookData, \"${MESSAGE}\", message)\n\n\t// 生成请求\n\t// 其中 GET 请求需转换为查询参数\n\treq := n.httpClient.R().SetContext(ctx).SetHeaderMultiValues(webhookHeaders)\n\treq.URL = webhookUrl.String()\n\treq.Method = webhookMethod\n\tif webhookMethod == http.MethodGet {\n\t\treq.SetQueryParams(webhookData.(map[string]string))\n\t} else {\n\t\tswitch webhookContentType {\n\t\tcase CONTENT_TYPE_JSON:\n\t\t\treq.SetBody(webhookData)\n\t\tcase CONTENT_TYPE_FORM:\n\t\t\treq.SetFormData(webhookData.(map[string]string))\n\t\tcase CONTENT_TYPE_MULTIPART:\n\t\t\treq.SetMultipartFormData(webhookData.(map[string]string))\n\t\t}\n\t}\n\n\t// 发送请求\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"webhook error: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn nil, fmt.Errorf(\"webhook error: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\tn.logger.Debug(\"webhook responded\", slog.String(\"response\", resp.String()))\n\n\treturn &notifier.NotifyResult{}, nil\n}\n\nfunc replaceJsonValueRecursively(data interface{}, oldStr, newStr string) interface{} {\n\tswitch v := data.(type) {\n\tcase map[string]any:\n\t\tfor k, val := range v {\n\t\t\tv[k] = replaceJsonValueRecursively(val, oldStr, newStr)\n\t\t}\n\tcase []any:\n\t\tfor i, val := range v {\n\t\t\tv[i] = replaceJsonValueRecursively(val, oldStr, newStr)\n\t\t}\n\tcase []string:\n\t\tfor i, s := range v {\n\t\t\tvar val interface{} = s\n\t\t\tvar newVal interface{} = replaceJsonValueRecursively(val, oldStr, newStr)\n\t\t\tv[i] = newVal.(string)\n\t\t}\n\tcase string:\n\t\treturn strings.ReplaceAll(v, oldStr, newStr)\n\t}\n\treturn data\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/webhook/webhook_test.go",
    "content": "package webhook_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/notifier/providers/webhook\"\n)\n\nconst (\n\tmockSubject = \"test_subject\"\n\tmockMessage = \"test_message\"\n)\n\nvar (\n\tfWebhookUrl         string\n\tfWebhookContentType string\n)\n\nfunc init() {\n\targsPrefix := \"WEBHOOK_\"\n\n\tflag.StringVar(&fWebhookUrl, argsPrefix+\"URL\", \"\", \"\")\n\tflag.StringVar(&fWebhookContentType, argsPrefix+\"CONTENTTYPE\", \"application/json\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./webhook_test.go -args \\\n\t--WEBHOOK_URL=\"https://example.com/your-webhook-url\" \\\n\t--WEBHOOK_CONTENTTYPE=\"application/json\"\n*/\nfunc TestNotify(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Notify\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"URL: %v\", fWebhookUrl),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewNotifier(&provider.NotifierConfig{\n\t\t\tWebhookUrl: fWebhookUrl,\n\t\t\tMethod:     \"POST\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Content-Type\": fWebhookContentType,\n\t\t\t},\n\t\t\tAllowInsecureConnections: true,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tres, err := provider.Notify(context.Background(), mockSubject, mockMessage)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/wecombot/wecombot.go",
    "content": "package wecombot\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n\t\"github.com/certimate-go/certimate/pkg/core/notifier\"\n)\n\ntype NotifierConfig struct {\n\t// 企业微信机器人 Webhook 地址。\n\tWebhookUrl string `json:\"webhookUrl\"`\n\t// 自定义消息数据。\n\t// 选填。\n\tCustomPayload string `json:\"customPayload,omitempty\"`\n}\n\ntype Notifier struct {\n\tconfig     *NotifierConfig\n\tlogger     *slog.Logger\n\thttpClient *resty.Client\n}\n\nvar _ notifier.Provider = (*Notifier)(nil)\n\nfunc NewNotifier(config *NotifierConfig) (*Notifier, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the notifier provider is nil\")\n\t}\n\n\tclient := resty.New().\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent)\n\n\treturn &Notifier{\n\t\tconfig:     config,\n\t\tlogger:     slog.Default(),\n\t\thttpClient: client,\n\t}, nil\n}\n\nfunc (n *Notifier) SetLogger(logger *slog.Logger) {\n\tif logger == nil {\n\t\tn.logger = slog.New(slog.DiscardHandler)\n\t} else {\n\t\tn.logger = logger\n\t}\n}\n\nfunc (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) {\n\twebhookUrl, err := url.Parse(n.config.WebhookUrl)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dingtalk api error: invalid webhook url: %w\", err)\n\t} else {\n\t\tconst hostname = \"qyapi.weixin.qq.com\"\n\t\tif webhookUrl.Hostname() != hostname {\n\t\t\tn.logger.Warn(fmt.Sprintf(\"the webhook url hostname is not '%s', please make sure it is correct\", hostname))\n\t\t}\n\t}\n\n\tvar webhookData map[string]any\n\tif n.config.CustomPayload == \"\" {\n\t\twebhookData = map[string]any{\n\t\t\t\"msgtype\": \"text\",\n\t\t\t\"text\": map[string]string{\n\t\t\t\t\"content\": subject + \"\\n\\n\" + message,\n\t\t\t},\n\t\t}\n\t} else {\n\t\terr = json.Unmarshal([]byte(n.config.CustomPayload), &webhookData)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to unmarshal webhook data: %w\", err)\n\t\t}\n\n\t\treplaceJsonValueRecursively(webhookData, \"${CERTIMATE_NOTIFIER_SUBJECT}\", subject)\n\t\treplaceJsonValueRecursively(webhookData, \"${CERTIMATE_NOTIFIER_MESSAGE}\", message)\n\n\t\treplaceJsonValueRecursively(webhookData, \"${SUBJECT}\", subject)\n\t\treplaceJsonValueRecursively(webhookData, \"${MESSAGE}\", message)\n\t}\n\n\t// REF: https://developer.work.weixin.qq.com/document/path/91770\n\tvar result struct {\n\t\tErrorCode    int    `json:\"errcode\"`\n\t\tErrorMessage string `json:\"errmsg\"`\n\t}\n\treq := n.httpClient.R().\n\t\tSetContext(ctx).\n\t\tSetBody(webhookData)\n\tresp, err := req.Post(webhookUrl.String())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"wecom api error: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn nil, fmt.Errorf(\"wecom api error: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t} else if err := json.Unmarshal(resp.Body(), &result); err != nil {\n\t\treturn nil, fmt.Errorf(\"wecom api error: %w (resp: %s)\", err, string(resp.Body()))\n\t} else if result.ErrorCode != 0 {\n\t\treturn nil, fmt.Errorf(\"wecom api error: errcode='%d', errmsg='%s'\", result.ErrorCode, result.ErrorMessage)\n\t}\n\n\treturn &notifier.NotifyResult{}, nil\n}\n\nfunc replaceJsonValueRecursively(data interface{}, oldStr, newStr string) interface{} {\n\tswitch v := data.(type) {\n\tcase map[string]any:\n\t\tfor k, val := range v {\n\t\t\tv[k] = replaceJsonValueRecursively(val, oldStr, newStr)\n\t\t}\n\tcase []any:\n\t\tfor i, val := range v {\n\t\t\tv[i] = replaceJsonValueRecursively(val, oldStr, newStr)\n\t\t}\n\tcase []string:\n\t\tfor i, s := range v {\n\t\t\tvar val interface{} = s\n\t\t\tvar newVal interface{} = replaceJsonValueRecursively(val, oldStr, newStr)\n\t\t\tv[i] = newVal.(string)\n\t\t}\n\tcase string:\n\t\treturn strings.ReplaceAll(v, oldStr, newStr)\n\t}\n\treturn data\n}\n"
  },
  {
    "path": "pkg/core/notifier/providers/wecombot/wecombot_test.go",
    "content": "package wecombot_test\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\tprovider \"github.com/certimate-go/certimate/pkg/core/notifier/providers/wecombot\"\n)\n\nconst (\n\tmockSubject = \"test_subject\"\n\tmockMessage = \"test_message\"\n)\n\nvar fWebhookUrl string\n\nfunc init() {\n\targsPrefix := \"WECOMBOT_\"\n\n\tflag.StringVar(&fWebhookUrl, argsPrefix+\"WEBHOOKURL\", \"\", \"\")\n}\n\n/*\nShell command to run this test:\n\n\tgo test -v ./wecombot_test.go -args \\\n\t--WECOMBOT_WEBHOOKURL=\"https://example.com/your-webhook-url\" \\\n*/\nfunc TestNotify(t *testing.T) {\n\tflag.Parse()\n\n\tt.Run(\"Notify\", func(t *testing.T) {\n\t\tt.Log(strings.Join([]string{\n\t\t\t\"args:\",\n\t\t\tfmt.Sprintf(\"WEBHOOKURL: %v\", fWebhookUrl),\n\t\t}, \"\\n\"))\n\n\t\tprovider, err := provider.NewNotifier(&provider.NotifierConfig{\n\t\t\tWebhookUrl: fWebhookUrl,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tres, err := provider.Notify(context.Background(), mockSubject, mockMessage)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"err: %+v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"ok: %v\", res)\n\t})\n}\n"
  },
  {
    "path": "pkg/core/shared.go",
    "content": "package core\n\nimport (\n\t\"log/slog\"\n)\n\ntype WithLogger interface {\n\tSetLogger(logger *slog.Logger)\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/README.md",
    "content": "移动云 Go SDK 文档: [https://ecloud.10086.cn/op-help-center/doc/article/53799](https://ecloud.10086.cn/op-help-center/doc/article/53799)\n\n移动云 Go SDK 下载地址: [https://ecloud.10086.cn/api/query/developer/nexus/service/rest/repository/browse/go-sdk/gitlab.ecloud.com/ecloud/](https://ecloud.10086.cn/api/query/developer/nexus/service/rest/repository/browse/go-sdk/gitlab.ecloud.com/ecloud/)\n\n---\n\n将其引入本地目录的原因是:\n\n1. 原始包必须通过移动云私有仓库获取, 为构建带来不便。\n2. 原始包存在部分内容错误, 需要自行修改, 如：\n   - 存在一些编译错误；\n   - 返回错误的时候, 未返回错误信息；\n   - 解析响应体错误。\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/client.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage ecloudsdkclouddns\n\nimport (\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkclouddns/model\"\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkcore\"\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkcore/config\"\n)\n\ntype Client struct {\n\tAPIClient   *ecloudsdkcore.APIClient\n\tconfig      *config.Config\n\thttpRequest *ecloudsdkcore.HttpRequest\n}\n\nfunc NewClient(config *config.Config) *Client {\n\tclient := &Client{}\n\tclient.config = config\n\tapiClient := ecloudsdkcore.NewAPIClient()\n\thttpRequest := ecloudsdkcore.NewDefaultHttpRequest()\n\thttpRequest.Product = product\n\thttpRequest.Version = version\n\thttpRequest.SdkVersion = sdkVersion\n\tclient.httpRequest = httpRequest\n\tclient.APIClient = apiClient\n\treturn client\n}\n\nfunc NewClientByCustomized(config *config.Config, httpRequest *ecloudsdkcore.HttpRequest) *Client {\n\tclient := &Client{}\n\tclient.config = config\n\tapiClient := ecloudsdkcore.NewAPIClient()\n\thttpRequest.Product = product\n\thttpRequest.Version = version\n\thttpRequest.SdkVersion = sdkVersion\n\tclient.httpRequest = httpRequest\n\tclient.APIClient = apiClient\n\treturn client\n}\n\nconst (\n\tproduct    string = \"clouddns\"\n\tversion    string = \"v1\"\n\tsdkVersion string = \"1.0.1\"\n)\n\n// CreateRecord 新增解析记录\nfunc (c *Client) CreateRecord(request *model.CreateRecordRequest) (*model.CreateRecordResponse, error) {\n\tc.httpRequest.Action = \"createRecord\"\n\tc.httpRequest.Body = request\n\treturnValue := &model.CreateRecordResponse{}\n\tif _, err := c.APIClient.Excute(c.httpRequest, c.config, returnValue); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn returnValue, nil\n\t}\n}\n\n// CreateRecordOpenapi 新增解析记录Openapi\nfunc (c *Client) CreateRecordOpenapi(request *model.CreateRecordOpenapiRequest) (*model.CreateRecordOpenapiResponse, error) {\n\tc.httpRequest.Action = \"createRecordOpenapi\"\n\tc.httpRequest.Body = request\n\treturnValue := &model.CreateRecordOpenapiResponse{}\n\tif _, err := c.APIClient.Excute(c.httpRequest, c.config, returnValue); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn returnValue, nil\n\t}\n}\n\n// DeleteRecord 删除解析记录\nfunc (c *Client) DeleteRecord(request *model.DeleteRecordRequest) (*model.DeleteRecordResponse, error) {\n\tc.httpRequest.Action = \"deleteRecord\"\n\tc.httpRequest.Body = request\n\treturnValue := &model.DeleteRecordResponse{}\n\tif _, err := c.APIClient.Excute(c.httpRequest, c.config, returnValue); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn returnValue, nil\n\t}\n}\n\n// DeleteRecordOpenapi 删除解析记录Openapi\nfunc (c *Client) DeleteRecordOpenapi(request *model.DeleteRecordOpenapiRequest) (*model.DeleteRecordOpenapiResponse, error) {\n\tc.httpRequest.Action = \"deleteRecordOpenapi\"\n\tc.httpRequest.Body = request\n\treturnValue := &model.DeleteRecordOpenapiResponse{}\n\tif _, err := c.APIClient.Excute(c.httpRequest, c.config, returnValue); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn returnValue, nil\n\t}\n}\n\n// ListRecord 查询解析记录\nfunc (c *Client) ListRecord(request *model.ListRecordRequest) (*model.ListRecordResponse, error) {\n\tc.httpRequest.Action = \"listRecord\"\n\tc.httpRequest.Body = request\n\treturnValue := &model.ListRecordResponse{}\n\tif _, err := c.APIClient.Excute(c.httpRequest, c.config, returnValue); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn returnValue, nil\n\t}\n}\n\n// ListRecordOpenapi 查询解析记录Openapi\nfunc (c *Client) ListRecordOpenapi(request *model.ListRecordOpenapiRequest) (*model.ListRecordOpenapiResponse, error) {\n\tc.httpRequest.Action = \"listRecordOpenapi\"\n\tc.httpRequest.Body = request\n\treturnValue := &model.ListRecordOpenapiResponse{}\n\tif _, err := c.APIClient.Excute(c.httpRequest, c.config, returnValue); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn returnValue, nil\n\t}\n}\n\n// ModifyRecord 修改解析记录\nfunc (c *Client) ModifyRecord(request *model.ModifyRecordRequest) (*model.ModifyRecordResponse, error) {\n\tc.httpRequest.Action = \"modifyRecord\"\n\tc.httpRequest.Body = request\n\treturnValue := &model.ModifyRecordResponse{}\n\tif _, err := c.APIClient.Excute(c.httpRequest, c.config, returnValue); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn returnValue, nil\n\t}\n}\n\n// ModifyRecordOpenapi 修改解析记录Openapi\nfunc (c *Client) ModifyRecordOpenapi(request *model.ModifyRecordOpenapiRequest) (*model.ModifyRecordOpenapiResponse, error) {\n\tc.httpRequest.Action = \"modifyRecordOpenapi\"\n\tc.httpRequest.Body = request\n\treturnValue := &model.ModifyRecordOpenapiResponse{}\n\tif _, err := c.APIClient.Excute(c.httpRequest, c.config, returnValue); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn returnValue, nil\n\t}\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/go.mod",
    "content": "module gitlab.ecloud.com/ecloud/ecloudsdkclouddns\n\ngo 1.14\n\nrequire gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0\n\nreplace gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 => ../ecloudsdkcore@v1.0.0\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_body.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\nimport (\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkcore/position\"\n)\n\ntype CreateRecordBodyTypeEnum string\n\n// List of Type\nconst (\n\tCreateRecordBodyTypeEnumA      CreateRecordBodyTypeEnum = \"A\"\n\tCreateRecordBodyTypeEnumAaaa   CreateRecordBodyTypeEnum = \"AAAA\"\n\tCreateRecordBodyTypeEnumCaa    CreateRecordBodyTypeEnum = \"CAA\"\n\tCreateRecordBodyTypeEnumCmauth CreateRecordBodyTypeEnum = \"CMAUTH\"\n\tCreateRecordBodyTypeEnumCname  CreateRecordBodyTypeEnum = \"CNAME\"\n\tCreateRecordBodyTypeEnumMx     CreateRecordBodyTypeEnum = \"MX\"\n\tCreateRecordBodyTypeEnumNs     CreateRecordBodyTypeEnum = \"NS\"\n\tCreateRecordBodyTypeEnumPtr    CreateRecordBodyTypeEnum = \"PTR\"\n\tCreateRecordBodyTypeEnumRp     CreateRecordBodyTypeEnum = \"RP\"\n\tCreateRecordBodyTypeEnumSpf    CreateRecordBodyTypeEnum = \"SPF\"\n\tCreateRecordBodyTypeEnumSrv    CreateRecordBodyTypeEnum = \"SRV\"\n\tCreateRecordBodyTypeEnumTxt    CreateRecordBodyTypeEnum = \"TXT\"\n\tCreateRecordBodyTypeEnumUrl    CreateRecordBodyTypeEnum = \"URL\"\n)\n\ntype CreateRecordBody struct {\n\tposition.Body\n\t// 主机头\n\tRr string `json:\"rr\"`\n\n\t// 域名名称\n\tDomainName string `json:\"domainName\"`\n\n\t// 备注\n\tDescription string `json:\"description,omitempty\"`\n\n\t// 线路ID\n\tLineId string `json:\"lineId\"`\n\n\t// MX优先级，若“记录类型”选择”MX”，则需要配置该参数,默认是5\n\tMxPri *int32 `json:\"mxPri,omitempty\"`\n\n\t// 记录类型\n\tType CreateRecordBodyTypeEnum `json:\"type\"`\n\n\t// 缓存的生命周期，默认可配置600s\n\tTtl *int32 `json:\"ttl,omitempty\"`\n\n\t// 记录值\n\tValue string `json:\"value\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_openapi_body.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\nimport (\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkcore/position\"\n)\n\ntype CreateRecordOpenapiBodyTypeEnum string\n\n// List of Type\nconst (\n\tCreateRecordOpenapiBodyTypeEnumA      CreateRecordOpenapiBodyTypeEnum = \"A\"\n\tCreateRecordOpenapiBodyTypeEnumAaaa   CreateRecordOpenapiBodyTypeEnum = \"AAAA\"\n\tCreateRecordOpenapiBodyTypeEnumCname  CreateRecordOpenapiBodyTypeEnum = \"CNAME\"\n\tCreateRecordOpenapiBodyTypeEnumMx     CreateRecordOpenapiBodyTypeEnum = \"MX\"\n\tCreateRecordOpenapiBodyTypeEnumTxt    CreateRecordOpenapiBodyTypeEnum = \"TXT\"\n\tCreateRecordOpenapiBodyTypeEnumNs     CreateRecordOpenapiBodyTypeEnum = \"NS\"\n\tCreateRecordOpenapiBodyTypeEnumSpf    CreateRecordOpenapiBodyTypeEnum = \"SPF\"\n\tCreateRecordOpenapiBodyTypeEnumSrv    CreateRecordOpenapiBodyTypeEnum = \"SRV\"\n\tCreateRecordOpenapiBodyTypeEnumCaa    CreateRecordOpenapiBodyTypeEnum = \"CAA\"\n\tCreateRecordOpenapiBodyTypeEnumCmauth CreateRecordOpenapiBodyTypeEnum = \"CMAUTH\"\n)\n\ntype CreateRecordOpenapiBody struct {\n\tposition.Body\n\t// 主机头\n\tRr string `json:\"rr\"`\n\n\t// 域名名称\n\tDomainName string `json:\"domainName\"`\n\n\t// 备注\n\tDescription string `json:\"description,omitempty\"`\n\n\t// 线路ID\n\tLineId string `json:\"lineId\"`\n\n\t// MX优先级，若“记录类型”选择”MX”，则需要配置该参数,默认是5\n\tMxPri *int32 `json:\"mxPri,omitempty\"`\n\n\t// 记录类型\n\tType CreateRecordOpenapiBodyTypeEnum `json:\"type\"`\n\n\t// 缓存的生命周期，默认可配置600s\n\tTtl *int32 `json:\"ttl,omitempty\"`\n\n\t// 记录值\n\tValue string `json:\"value\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_openapi_request.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype CreateRecordOpenapiRequest struct {\n\tCreateRecordOpenapiBody *CreateRecordOpenapiBody `json:\"createRecordOpenapiBody,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_openapi_response.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype CreateRecordOpenapiResponseStateEnum string\n\n// List of State\nconst (\n\tCreateRecordOpenapiResponseStateEnumError     CreateRecordOpenapiResponseStateEnum = \"ERROR\"\n\tCreateRecordOpenapiResponseStateEnumException CreateRecordOpenapiResponseStateEnum = \"EXCEPTION\"\n\tCreateRecordOpenapiResponseStateEnumForbidden CreateRecordOpenapiResponseStateEnum = \"FORBIDDEN\"\n\tCreateRecordOpenapiResponseStateEnumOk        CreateRecordOpenapiResponseStateEnum = \"OK\"\n)\n\ntype CreateRecordOpenapiResponse struct {\n\tRequestId string `json:\"requestId,omitempty\"`\n\n\tErrorMessage string `json:\"errorMessage,omitempty\"`\n\n\tErrorCode string `json:\"errorCode,omitempty\"`\n\n\tState CreateRecordOpenapiResponseStateEnum `json:\"state,omitempty\"`\n\n\tBody *CreateRecordOpenapiResponseBody `json:\"body,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_openapi_response_body.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype CreateRecordOpenapiResponseBodyTypeEnum string\n\n// List of Type\nconst (\n\tCreateRecordOpenapiResponseBodyTypeEnumA      CreateRecordOpenapiResponseBodyTypeEnum = \"A\"\n\tCreateRecordOpenapiResponseBodyTypeEnumAaaa   CreateRecordOpenapiResponseBodyTypeEnum = \"AAAA\"\n\tCreateRecordOpenapiResponseBodyTypeEnumCname  CreateRecordOpenapiResponseBodyTypeEnum = \"CNAME\"\n\tCreateRecordOpenapiResponseBodyTypeEnumMx     CreateRecordOpenapiResponseBodyTypeEnum = \"MX\"\n\tCreateRecordOpenapiResponseBodyTypeEnumTxt    CreateRecordOpenapiResponseBodyTypeEnum = \"TXT\"\n\tCreateRecordOpenapiResponseBodyTypeEnumNs     CreateRecordOpenapiResponseBodyTypeEnum = \"NS\"\n\tCreateRecordOpenapiResponseBodyTypeEnumSpf    CreateRecordOpenapiResponseBodyTypeEnum = \"SPF\"\n\tCreateRecordOpenapiResponseBodyTypeEnumSrv    CreateRecordOpenapiResponseBodyTypeEnum = \"SRV\"\n\tCreateRecordOpenapiResponseBodyTypeEnumCaa    CreateRecordOpenapiResponseBodyTypeEnum = \"CAA\"\n\tCreateRecordOpenapiResponseBodyTypeEnumCmauth CreateRecordOpenapiResponseBodyTypeEnum = \"CMAUTH\"\n)\n\ntype CreateRecordOpenapiResponseBodyStateEnum string\n\n// List of State\nconst (\n\tCreateRecordOpenapiResponseBodyStateEnumDisabled CreateRecordOpenapiResponseBodyStateEnum = \"DISABLED\"\n\tCreateRecordOpenapiResponseBodyStateEnumEnabled  CreateRecordOpenapiResponseBodyStateEnum = \"ENABLED\"\n)\n\ntype CreateRecordOpenapiResponseBody struct {\n\t// 主机头\n\tRr string `json:\"rr,omitempty\"`\n\n\t// 修改时间\n\tModifiedTime string `json:\"modifiedTime,omitempty\"`\n\n\t// 线路中文名\n\tLineZh string `json:\"lineZh,omitempty\"`\n\n\t// 备注\n\tDescription string `json:\"description,omitempty\"`\n\n\t// 线路ID\n\tLineId string `json:\"lineId,omitempty\"`\n\n\t// 权重值\n\tWeight *int32 `json:\"weight,omitempty\"`\n\n\t// MX优先级\n\tMxPri *int32 `json:\"mxPri,omitempty\"`\n\n\t// 记录类型\n\tType CreateRecordOpenapiResponseBodyTypeEnum `json:\"type,omitempty\"`\n\n\t// 缓存的生命周期\n\tTtl *int32 `json:\"ttl,omitempty\"`\n\n\t// 标签\n\tTags *[]CreateRecordOpenapiResponseTags `json:\"tags,omitempty\"`\n\n\t// 解析记录ID\n\tRecordId string `json:\"recordId,omitempty\"`\n\n\t// 域名名称\n\tDomainName string `json:\"domainName,omitempty\"`\n\n\t// 线路英文名\n\tLineEn string `json:\"lineEn,omitempty\"`\n\n\t// 状态\n\tState CreateRecordOpenapiResponseBodyStateEnum `json:\"state,omitempty\"`\n\n\t// 记录值\n\tValue string `json:\"value,omitempty\"`\n\n\t// 定时发布时间\n\tPubdate string `json:\"pubdate,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_openapi_response_tags.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype CreateRecordOpenapiResponseTags struct {\n\t// 标签ID\n\tTagId string `json:\"tagId,omitempty\"`\n\n\t// 标签名称\n\tValue string `json:\"value,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_request.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype CreateRecordRequest struct {\n\tCreateRecordBody *CreateRecordBody `json:\"createRecordBody,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_response.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype CreateRecordResponseStateEnum string\n\n// List of State\nconst (\n\tCreateRecordResponseStateEnumError     CreateRecordResponseStateEnum = \"ERROR\"\n\tCreateRecordResponseStateEnumException CreateRecordResponseStateEnum = \"EXCEPTION\"\n\tCreateRecordResponseStateEnumForbidden CreateRecordResponseStateEnum = \"FORBIDDEN\"\n\tCreateRecordResponseStateEnumOk        CreateRecordResponseStateEnum = \"OK\"\n)\n\ntype CreateRecordResponse struct {\n\tRequestId string `json:\"requestId,omitempty\"`\n\n\tErrorMessage string `json:\"errorMessage,omitempty\"`\n\n\tErrorCode string `json:\"errorCode,omitempty\"`\n\n\tState CreateRecordResponseStateEnum `json:\"state,omitempty\"`\n\n\tBody *CreateRecordResponseBody `json:\"body,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_response_body.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype CreateRecordResponseBodyTypeEnum string\n\n// List of Type\nconst (\n\tCreateRecordResponseBodyTypeEnumA      CreateRecordResponseBodyTypeEnum = \"A\"\n\tCreateRecordResponseBodyTypeEnumAaaa   CreateRecordResponseBodyTypeEnum = \"AAAA\"\n\tCreateRecordResponseBodyTypeEnumCaa    CreateRecordResponseBodyTypeEnum = \"CAA\"\n\tCreateRecordResponseBodyTypeEnumCmauth CreateRecordResponseBodyTypeEnum = \"CMAUTH\"\n\tCreateRecordResponseBodyTypeEnumCname  CreateRecordResponseBodyTypeEnum = \"CNAME\"\n\tCreateRecordResponseBodyTypeEnumMx     CreateRecordResponseBodyTypeEnum = \"MX\"\n\tCreateRecordResponseBodyTypeEnumNs     CreateRecordResponseBodyTypeEnum = \"NS\"\n\tCreateRecordResponseBodyTypeEnumPtr    CreateRecordResponseBodyTypeEnum = \"PTR\"\n\tCreateRecordResponseBodyTypeEnumRp     CreateRecordResponseBodyTypeEnum = \"RP\"\n\tCreateRecordResponseBodyTypeEnumSpf    CreateRecordResponseBodyTypeEnum = \"SPF\"\n\tCreateRecordResponseBodyTypeEnumSrv    CreateRecordResponseBodyTypeEnum = \"SRV\"\n\tCreateRecordResponseBodyTypeEnumTxt    CreateRecordResponseBodyTypeEnum = \"TXT\"\n\tCreateRecordResponseBodyTypeEnumUrl    CreateRecordResponseBodyTypeEnum = \"URL\"\n)\n\ntype CreateRecordResponseBodyTimedStatusEnum string\n\n// List of TimedStatus\nconst (\n\tCreateRecordResponseBodyTimedStatusEnumDisabled CreateRecordResponseBodyTimedStatusEnum = \"DISABLED\"\n\tCreateRecordResponseBodyTimedStatusEnumEnabled  CreateRecordResponseBodyTimedStatusEnum = \"ENABLED\"\n\tCreateRecordResponseBodyTimedStatusEnumTimed    CreateRecordResponseBodyTimedStatusEnum = \"TIMED\"\n)\n\ntype CreateRecordResponseBodyStateEnum string\n\n// List of State\nconst (\n\tCreateRecordResponseBodyStateEnumDisabled CreateRecordResponseBodyStateEnum = \"DISABLED\"\n\tCreateRecordResponseBodyStateEnumEnabled  CreateRecordResponseBodyStateEnum = \"ENABLED\"\n)\n\ntype CreateRecordResponseBody struct {\n\t// 主机头\n\tRr string `json:\"rr,omitempty\"`\n\n\t// 修改时间\n\tModifiedTime string `json:\"modifiedTime,omitempty\"`\n\n\t// 线路中文名\n\tLineZh string `json:\"lineZh,omitempty\"`\n\n\t// 备注\n\tDescription string `json:\"description,omitempty\"`\n\n\t// 线路ID\n\tLineId string `json:\"lineId,omitempty\"`\n\n\t// 权重值\n\tWeight *int32 `json:\"weight,omitempty\"`\n\n\t// MX优先级\n\tMxPri *int32 `json:\"mxPri,omitempty\"`\n\n\t// 记录类型\n\tType CreateRecordResponseBodyTypeEnum `json:\"type,omitempty\"`\n\n\t// 缓存的生命周期\n\tTtl *int32 `json:\"ttl,omitempty\"`\n\n\t// 标签\n\tTags *[]CreateRecordResponseTags `json:\"tags,omitempty\"`\n\n\t// 解析记录ID\n\tRecordId string `json:\"recordId,omitempty\"`\n\n\t// 定时状态\n\tTimedStatus CreateRecordResponseBodyTimedStatusEnum `json:\"timedStatus,omitempty\"`\n\n\t// 域名名称\n\tDomainName string `json:\"domainName,omitempty\"`\n\n\t// 线路英文名\n\tLineEn string `json:\"lineEn,omitempty\"`\n\n\t// 状态\n\tState CreateRecordResponseBodyStateEnum `json:\"state,omitempty\"`\n\n\t// 记录值\n\tValue string `json:\"value,omitempty\"`\n\n\t// 定时发布时间\n\tPubdate string `json:\"pubdate,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_response_tags.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype CreateRecordResponseTags struct {\n\t// 标签ID\n\tTagId string `json:\"tagId,omitempty\"`\n\n\t// 标签名称\n\tValue string `json:\"value,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/delete_record_body.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\nimport (\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkcore/position\"\n)\n\ntype DeleteRecordBody struct {\n\tposition.Body\n\t// 解析记录ID列表\n\tRecordIdList []string `json:\"recordIdList\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/delete_record_openapi_body.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\nimport (\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkcore/position\"\n)\n\ntype DeleteRecordOpenapiBody struct {\n\tposition.Body\n\t// 待删除的解析记录ID请求体\n\tRecordIdList []string `json:\"recordIdList\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/delete_record_openapi_request.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype DeleteRecordOpenapiRequest struct {\n\tDeleteRecordOpenapiBody *DeleteRecordOpenapiBody `json:\"deleteRecordOpenapiBody,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/delete_record_openapi_response.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype DeleteRecordOpenapiResponseStateEnum string\n\n// List of State\nconst (\n\tDeleteRecordOpenapiResponseStateEnumError     DeleteRecordOpenapiResponseStateEnum = \"ERROR\"\n\tDeleteRecordOpenapiResponseStateEnumException DeleteRecordOpenapiResponseStateEnum = \"EXCEPTION\"\n\tDeleteRecordOpenapiResponseStateEnumForbidden DeleteRecordOpenapiResponseStateEnum = \"FORBIDDEN\"\n\tDeleteRecordOpenapiResponseStateEnumOk        DeleteRecordOpenapiResponseStateEnum = \"OK\"\n)\n\ntype DeleteRecordOpenapiResponse struct {\n\tRequestId string `json:\"requestId,omitempty\"`\n\n\tErrorMessage string `json:\"errorMessage,omitempty\"`\n\n\tErrorCode string `json:\"errorCode,omitempty\"`\n\n\tState DeleteRecordOpenapiResponseStateEnum `json:\"state,omitempty\"`\n\n\tBody *[]DeleteRecordOpenapiResponseBody `json:\"body,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/delete_record_openapi_response_body.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype DeleteRecordOpenapiResponseBodyCodeEnum string\n\n// List of Code\nconst (\n\tDeleteRecordOpenapiResponseBodyCodeEnumError   DeleteRecordOpenapiResponseBodyCodeEnum = \"ERROR\"\n\tDeleteRecordOpenapiResponseBodyCodeEnumSuccess DeleteRecordOpenapiResponseBodyCodeEnum = \"SUCCESS\"\n)\n\ntype DeleteRecordOpenapiResponseBody struct {\n\t// 结果说明\n\tMsg string `json:\"msg,omitempty\"`\n\n\t// 解析记录ID\n\tRecordId string `json:\"recordId,omitempty\"`\n\n\t// 结果码\n\tCode DeleteRecordOpenapiResponseBodyCodeEnum `json:\"code,omitempty\"`\n\n\t// 域名\n\tDomainName string `json:\"domainName,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/delete_record_request.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype DeleteRecordRequest struct {\n\tDeleteRecordBody *DeleteRecordBody `json:\"deleteRecordBody,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/delete_record_response.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype DeleteRecordResponseStateEnum string\n\n// List of State\nconst (\n\tDeleteRecordResponseStateEnumError     DeleteRecordResponseStateEnum = \"ERROR\"\n\tDeleteRecordResponseStateEnumException DeleteRecordResponseStateEnum = \"EXCEPTION\"\n\tDeleteRecordResponseStateEnumForbidden DeleteRecordResponseStateEnum = \"FORBIDDEN\"\n\tDeleteRecordResponseStateEnumOk        DeleteRecordResponseStateEnum = \"OK\"\n)\n\ntype DeleteRecordResponse struct {\n\tRequestId string `json:\"requestId,omitempty\"`\n\n\tErrorMessage string `json:\"errorMessage,omitempty\"`\n\n\tErrorCode string `json:\"errorCode,omitempty\"`\n\n\tState DeleteRecordResponseStateEnum `json:\"state,omitempty\"`\n\n\tBody *[]DeleteRecordResponseBody `json:\"body,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/delete_record_response_body.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype DeleteRecordResponseBodyCodeEnum string\n\n// List of Code\nconst (\n\tDeleteRecordResponseBodyCodeEnumError   DeleteRecordResponseBodyCodeEnum = \"ERROR\"\n\tDeleteRecordResponseBodyCodeEnumSuccess DeleteRecordResponseBodyCodeEnum = \"SUCCESS\"\n)\n\ntype DeleteRecordResponseBody struct {\n\t// 结果说明\n\tMsg string `json:\"msg,omitempty\"`\n\n\t// 解析记录ID\n\tRecordId string `json:\"recordId,omitempty\"`\n\n\t// 结果码\n\tCode DeleteRecordResponseBodyCodeEnum `json:\"code,omitempty\"`\n\n\t// 域名\n\tDomainName string `json:\"domainName,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_body.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\nimport (\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkcore/position\"\n)\n\ntype ListRecordBody struct {\n\tposition.Body\n\t// 域名\n\tDomainName string `json:\"domainName\"`\n\n\t// 可以匹配主机头rr、记录值value、备注description，并且是模糊搜索\n\tDataLike string `json:\"dataLike,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_openapi_body.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\nimport (\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkcore/position\"\n)\n\ntype ListRecordOpenapiBody struct {\n\tposition.Body\n\t// 域名\n\tDomainName string `json:\"domainName\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_openapi_query.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\nimport (\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkcore/position\"\n)\n\ntype ListRecordOpenapiQuery struct {\n\tposition.Query\n\t// 页大小\n\tPageSize *int32 `json:\"pageSize,omitempty\"`\n\n\t// 当前页\n\tPage *int32 `json:\"page,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_openapi_request.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype ListRecordOpenapiRequest struct {\n\tListRecordOpenapiQuery *ListRecordOpenapiQuery `json:\"listRecordOpenapiQuery,omitempty\"`\n\n\tListRecordOpenapiBody *ListRecordOpenapiBody `json:\"listRecordOpenapiBody,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_openapi_response.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype ListRecordOpenapiResponseStateEnum string\n\n// List of State\nconst (\n\tListRecordOpenapiResponseStateEnumError     ListRecordOpenapiResponseStateEnum = \"ERROR\"\n\tListRecordOpenapiResponseStateEnumException ListRecordOpenapiResponseStateEnum = \"EXCEPTION\"\n\tListRecordOpenapiResponseStateEnumForbidden ListRecordOpenapiResponseStateEnum = \"FORBIDDEN\"\n\tListRecordOpenapiResponseStateEnumOk        ListRecordOpenapiResponseStateEnum = \"OK\"\n)\n\ntype ListRecordOpenapiResponse struct {\n\tRequestId string `json:\"requestId,omitempty\"`\n\n\tErrorMessage string `json:\"errorMessage,omitempty\"`\n\n\tErrorCode string `json:\"errorCode,omitempty\"`\n\n\tState ListRecordOpenapiResponseStateEnum `json:\"state,omitempty\"`\n\n\tBody *ListRecordOpenapiResponseBody `json:\"body,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_openapi_response_body.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype ListRecordOpenapiResponseBody struct {\n\t// 当前页的具体数据列表\n\tData *[]ListRecordOpenapiResponseData `json:\"data,omitempty\"`\n\n\t// 总数据量\n\tTotalNum *int32 `json:\"totalNum,omitempty\"`\n\n\t// 总页数\n\tTotalPages *int32 `json:\"totalPages,omitempty\"`\n\n\t// 页大小\n\tPageSize *int32 `json:\"pageSize,omitempty\"`\n\n\t// 当前页码，从0开始，0表示第一页\n\tPage *int32 `json:\"page,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_openapi_response_data.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype ListRecordOpenapiResponseDataTypeEnum string\n\n// List of Type\nconst (\n\tListRecordOpenapiResponseDataTypeEnumA      ListRecordOpenapiResponseDataTypeEnum = \"A\"\n\tListRecordOpenapiResponseDataTypeEnumAaaa   ListRecordOpenapiResponseDataTypeEnum = \"AAAA\"\n\tListRecordOpenapiResponseDataTypeEnumCname  ListRecordOpenapiResponseDataTypeEnum = \"CNAME\"\n\tListRecordOpenapiResponseDataTypeEnumMx     ListRecordOpenapiResponseDataTypeEnum = \"MX\"\n\tListRecordOpenapiResponseDataTypeEnumTxt    ListRecordOpenapiResponseDataTypeEnum = \"TXT\"\n\tListRecordOpenapiResponseDataTypeEnumNs     ListRecordOpenapiResponseDataTypeEnum = \"NS\"\n\tListRecordOpenapiResponseDataTypeEnumSpf    ListRecordOpenapiResponseDataTypeEnum = \"SPF\"\n\tListRecordOpenapiResponseDataTypeEnumSrv    ListRecordOpenapiResponseDataTypeEnum = \"SRV\"\n\tListRecordOpenapiResponseDataTypeEnumCaa    ListRecordOpenapiResponseDataTypeEnum = \"CAA\"\n\tListRecordOpenapiResponseDataTypeEnumCmauth ListRecordOpenapiResponseDataTypeEnum = \"CMAUTH\"\n)\n\ntype ListRecordOpenapiResponseDataTimedStatusEnum string\n\n// List of TimedStatus\nconst (\n\tListRecordOpenapiResponseDataTimedStatusEnumDisabled ListRecordOpenapiResponseDataTimedStatusEnum = \"DISABLED\"\n\tListRecordOpenapiResponseDataTimedStatusEnumEnabled  ListRecordOpenapiResponseDataTimedStatusEnum = \"ENABLED\"\n\tListRecordOpenapiResponseDataTimedStatusEnumTimed    ListRecordOpenapiResponseDataTimedStatusEnum = \"TIMED\"\n)\n\ntype ListRecordOpenapiResponseDataStateEnum string\n\n// List of State\nconst (\n\tListRecordOpenapiResponseDataStateEnumDisabled ListRecordOpenapiResponseDataStateEnum = \"DISABLED\"\n\tListRecordOpenapiResponseDataStateEnumEnabled  ListRecordOpenapiResponseDataStateEnum = \"ENABLED\"\n)\n\ntype ListRecordOpenapiResponseData struct {\n\t// 主机头\n\tRr string `json:\"rr,omitempty\"`\n\n\t// 修改时间\n\tModifiedTime string `json:\"modifiedTime,omitempty\"`\n\n\t// 线路中文名\n\tLineZh string `json:\"lineZh,omitempty\"`\n\n\t// 备注\n\tDescription string `json:\"description,omitempty\"`\n\n\t// 线路ID\n\tLineId string `json:\"lineId,omitempty\"`\n\n\t// 权重值\n\tWeight *int32 `json:\"weight,omitempty\"`\n\n\t// MX优先级\n\tMxPri *int32 `json:\"mxPri,omitempty\"`\n\n\t// 记录类型\n\tType ListRecordOpenapiResponseDataTypeEnum `json:\"type,omitempty\"`\n\n\t// 缓存的生命周期\n\tTtl *int32 `json:\"ttl,omitempty\"`\n\n\t// 标签\n\tTags *[]ListRecordOpenapiResponseTags `json:\"tags,omitempty\"`\n\n\t// 解析记录ID\n\tRecordId string `json:\"recordId,omitempty\"`\n\n\t// 定时状态\n\tTimedStatus ListRecordOpenapiResponseDataTimedStatusEnum `json:\"timedStatus,omitempty\"`\n\n\t// 域名名称\n\tDomainName string `json:\"domainName,omitempty\"`\n\n\t// 线路英文名\n\tLineEn string `json:\"lineEn,omitempty\"`\n\n\t// 状态\n\tState ListRecordOpenapiResponseDataStateEnum `json:\"state,omitempty\"`\n\n\t// 记录值\n\tValue string `json:\"value,omitempty\"`\n\n\t// 定时发布时间\n\tPubdate string `json:\"pubdate,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_openapi_response_tags.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype ListRecordOpenapiResponseTags struct {\n\t// 标签ID\n\tTagId string `json:\"tagId,omitempty\"`\n\n\t// 标签名称\n\tValue string `json:\"value,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_query.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\nimport (\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkcore/position\"\n)\n\ntype ListRecordQuery struct {\n\tposition.Query\n\t// 页大小\n\tPageSize *int32 `json:\"pageSize,omitempty\"`\n\n\t// 当前页\n\tCurrentPage *int32 `json:\"currentPage,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_request.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype ListRecordRequest struct {\n\tListRecordBody *ListRecordBody `json:\"listRecordBody,omitempty\"`\n\n\tListRecordQuery *ListRecordQuery `json:\"listRecordQuery,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_response.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype ListRecordResponseStateEnum string\n\n// List of State\nconst (\n\tListRecordResponseStateEnumError     ListRecordResponseStateEnum = \"ERROR\"\n\tListRecordResponseStateEnumException ListRecordResponseStateEnum = \"EXCEPTION\"\n\tListRecordResponseStateEnumForbidden ListRecordResponseStateEnum = \"FORBIDDEN\"\n\tListRecordResponseStateEnumOk        ListRecordResponseStateEnum = \"OK\"\n)\n\ntype ListRecordResponse struct {\n\tRequestId string `json:\"requestId,omitempty\"`\n\n\tErrorMessage string `json:\"errorMessage,omitempty\"`\n\n\tErrorCode string `json:\"errorCode,omitempty\"`\n\n\tState ListRecordResponseStateEnum `json:\"state,omitempty\"`\n\n\tBody *ListRecordResponseBody `json:\"body,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_response_body.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype ListRecordResponseBody struct {\n\t// 总页数\n\tTotalPages *int32 `json:\"totalPages,omitempty\"`\n\n\t// 当前页码，从0开始，0表示第一页\n\tCurrentPage *int32 `json:\"currentPage,omitempty\"`\n\n\t// 当前页的具体数据列表\n\tResults *[]ListRecordResponseResults `json:\"results,omitempty\"`\n\n\t// 总数据量\n\tTotalElements *int64 `json:\"totalElements,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_response_results.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype ListRecordResponseResultsTypeEnum string\n\n// List of Type\nconst (\n\tListRecordResponseResultsTypeEnumA      ListRecordResponseResultsTypeEnum = \"A\"\n\tListRecordResponseResultsTypeEnumAaaa   ListRecordResponseResultsTypeEnum = \"AAAA\"\n\tListRecordResponseResultsTypeEnumCaa    ListRecordResponseResultsTypeEnum = \"CAA\"\n\tListRecordResponseResultsTypeEnumCmauth ListRecordResponseResultsTypeEnum = \"CMAUTH\"\n\tListRecordResponseResultsTypeEnumCname  ListRecordResponseResultsTypeEnum = \"CNAME\"\n\tListRecordResponseResultsTypeEnumMx     ListRecordResponseResultsTypeEnum = \"MX\"\n\tListRecordResponseResultsTypeEnumNs     ListRecordResponseResultsTypeEnum = \"NS\"\n\tListRecordResponseResultsTypeEnumPtr    ListRecordResponseResultsTypeEnum = \"PTR\"\n\tListRecordResponseResultsTypeEnumRp     ListRecordResponseResultsTypeEnum = \"RP\"\n\tListRecordResponseResultsTypeEnumSpf    ListRecordResponseResultsTypeEnum = \"SPF\"\n\tListRecordResponseResultsTypeEnumSrv    ListRecordResponseResultsTypeEnum = \"SRV\"\n\tListRecordResponseResultsTypeEnumTxt    ListRecordResponseResultsTypeEnum = \"TXT\"\n\tListRecordResponseResultsTypeEnumUrl    ListRecordResponseResultsTypeEnum = \"URL\"\n)\n\ntype ListRecordResponseResultsTimedStatusEnum string\n\n// List of TimedStatus\nconst (\n\tListRecordResponseResultsTimedStatusEnumDisabled ListRecordResponseResultsTimedStatusEnum = \"DISABLED\"\n\tListRecordResponseResultsTimedStatusEnumEnabled  ListRecordResponseResultsTimedStatusEnum = \"ENABLED\"\n\tListRecordResponseResultsTimedStatusEnumTimed    ListRecordResponseResultsTimedStatusEnum = \"TIMED\"\n)\n\ntype ListRecordResponseResultsStateEnum string\n\n// List of State\nconst (\n\tListRecordResponseResultsStateEnumDisabled ListRecordResponseResultsStateEnum = \"DISABLED\"\n\tListRecordResponseResultsStateEnumEnabled  ListRecordResponseResultsStateEnum = \"ENABLED\"\n)\n\ntype ListRecordResponseResults struct {\n\t// 主机头\n\tRr string `json:\"rr,omitempty\"`\n\n\t// 修改时间\n\tModifiedTime string `json:\"modifiedTime,omitempty\"`\n\n\t// 线路中文名\n\tLineZh string `json:\"lineZh,omitempty\"`\n\n\t// 备注\n\tDescription string `json:\"description,omitempty\"`\n\n\t// 线路ID\n\tLineId string `json:\"lineId,omitempty\"`\n\n\t// 权重值\n\tWeight *int32 `json:\"weight,omitempty\"`\n\n\t// MX优先级\n\tMxPri *int32 `json:\"mxPri,omitempty\"`\n\n\t// 记录类型\n\tType ListRecordResponseResultsTypeEnum `json:\"type,omitempty\"`\n\n\t// 缓存的生命周期\n\tTtl *int32 `json:\"ttl,omitempty\"`\n\n\t// 解析记录ID\n\tRecordId string `json:\"recordId,omitempty\"`\n\n\t// 定时状态\n\tTimedStatus ListRecordResponseResultsTimedStatusEnum `json:\"timedStatus,omitempty\"`\n\n\t// 域名名称\n\tDomainName string `json:\"domainName,omitempty\"`\n\n\t// 线路英文名\n\tLineEn string `json:\"lineEn,omitempty\"`\n\n\t// 状态\n\tState ListRecordResponseResultsStateEnum `json:\"state,omitempty\"`\n\n\t// 记录值\n\tValue string `json:\"value,omitempty\"`\n\n\t// 定时发布时间\n\tPubdate string `json:\"pubdate,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_body.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\nimport (\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkcore/position\"\n)\n\ntype ModifyRecordBodyTypeEnum string\n\n// List of Type\nconst (\n\tModifyRecordBodyTypeEnumA      ModifyRecordBodyTypeEnum = \"A\"\n\tModifyRecordBodyTypeEnumAaaa   ModifyRecordBodyTypeEnum = \"AAAA\"\n\tModifyRecordBodyTypeEnumCaa    ModifyRecordBodyTypeEnum = \"CAA\"\n\tModifyRecordBodyTypeEnumCmauth ModifyRecordBodyTypeEnum = \"CMAUTH\"\n\tModifyRecordBodyTypeEnumCname  ModifyRecordBodyTypeEnum = \"CNAME\"\n\tModifyRecordBodyTypeEnumMx     ModifyRecordBodyTypeEnum = \"MX\"\n\tModifyRecordBodyTypeEnumNs     ModifyRecordBodyTypeEnum = \"NS\"\n\tModifyRecordBodyTypeEnumPtr    ModifyRecordBodyTypeEnum = \"PTR\"\n\tModifyRecordBodyTypeEnumRp     ModifyRecordBodyTypeEnum = \"RP\"\n\tModifyRecordBodyTypeEnumSpf    ModifyRecordBodyTypeEnum = \"SPF\"\n\tModifyRecordBodyTypeEnumSrv    ModifyRecordBodyTypeEnum = \"SRV\"\n\tModifyRecordBodyTypeEnumTxt    ModifyRecordBodyTypeEnum = \"TXT\"\n\tModifyRecordBodyTypeEnumUrl    ModifyRecordBodyTypeEnum = \"URL\"\n)\n\ntype ModifyRecordBody struct {\n\tposition.Body\n\t// 解析记录ID\n\tRecordId string `json:\"recordId\"`\n\n\t// 主机头\n\tRr string `json:\"rr,omitempty\"`\n\n\t// 域名名称\n\tDomainName string `json:\"domainName\"`\n\n\t// 备注\n\tDescription string `json:\"description,omitempty\"`\n\n\t// 线路ID\n\tLineId string `json:\"lineId,omitempty\"`\n\n\t// MX优先级\n\tMxPri *int32 `json:\"mxPri,omitempty\"`\n\n\t// 记录类型\n\tType ModifyRecordBodyTypeEnum `json:\"type,omitempty\"`\n\n\t// 缓存的生命周期\n\tTtl *int32 `json:\"ttl,omitempty\"`\n\n\t// 记录值\n\tValue string `json:\"value,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_openapi_body.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\nimport (\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkcore/position\"\n)\n\ntype ModifyRecordOpenapiBodyTypeEnum string\n\n// List of Type\nconst (\n\tModifyRecordOpenapiBodyTypeEnumA      ModifyRecordOpenapiBodyTypeEnum = \"A\"\n\tModifyRecordOpenapiBodyTypeEnumAaaa   ModifyRecordOpenapiBodyTypeEnum = \"AAAA\"\n\tModifyRecordOpenapiBodyTypeEnumCname  ModifyRecordOpenapiBodyTypeEnum = \"CNAME\"\n\tModifyRecordOpenapiBodyTypeEnumMx     ModifyRecordOpenapiBodyTypeEnum = \"MX\"\n\tModifyRecordOpenapiBodyTypeEnumTxt    ModifyRecordOpenapiBodyTypeEnum = \"TXT\"\n\tModifyRecordOpenapiBodyTypeEnumNs     ModifyRecordOpenapiBodyTypeEnum = \"NS\"\n\tModifyRecordOpenapiBodyTypeEnumSpf    ModifyRecordOpenapiBodyTypeEnum = \"SPF\"\n\tModifyRecordOpenapiBodyTypeEnumSrv    ModifyRecordOpenapiBodyTypeEnum = \"SRV\"\n\tModifyRecordOpenapiBodyTypeEnumCaa    ModifyRecordOpenapiBodyTypeEnum = \"CAA\"\n\tModifyRecordOpenapiBodyTypeEnumCmauth ModifyRecordOpenapiBodyTypeEnum = \"CMAUTH\"\n)\n\ntype ModifyRecordOpenapiBody struct {\n\tposition.Body\n\t// 解析记录ID\n\tRecordId string `json:\"recordId\"`\n\n\t// 主机头\n\tRr string `json:\"rr,omitempty\"`\n\n\t// 域名名称\n\tDomainName string `json:\"domainName\"`\n\n\t// 备注\n\tDescription string `json:\"description,omitempty\"`\n\n\t// 线路ID\n\tLineId string `json:\"lineId,omitempty\"`\n\n\t// MX优先级\n\tMxPri *int32 `json:\"mxPri,omitempty\"`\n\n\t// 记录类型\n\tType ModifyRecordOpenapiBodyTypeEnum `json:\"type,omitempty\"`\n\n\t// 缓存的生命周期\n\tTtl *int32 `json:\"ttl,omitempty\"`\n\n\t// 记录值\n\tValue string `json:\"value,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_openapi_request.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype ModifyRecordOpenapiRequest struct {\n\tModifyRecordOpenapiBody *ModifyRecordOpenapiBody `json:\"modifyRecordOpenapiBody,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_openapi_response.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype ModifyRecordOpenapiResponseStateEnum string\n\n// List of State\nconst (\n\tModifyRecordOpenapiResponseStateEnumError     ModifyRecordOpenapiResponseStateEnum = \"ERROR\"\n\tModifyRecordOpenapiResponseStateEnumException ModifyRecordOpenapiResponseStateEnum = \"EXCEPTION\"\n\tModifyRecordOpenapiResponseStateEnumForbidden ModifyRecordOpenapiResponseStateEnum = \"FORBIDDEN\"\n\tModifyRecordOpenapiResponseStateEnumOk        ModifyRecordOpenapiResponseStateEnum = \"OK\"\n)\n\ntype ModifyRecordOpenapiResponse struct {\n\tRequestId string `json:\"requestId,omitempty\"`\n\n\tErrorMessage string `json:\"errorMessage,omitempty\"`\n\n\tErrorCode string `json:\"errorCode,omitempty\"`\n\n\tState ModifyRecordOpenapiResponseStateEnum `json:\"state,omitempty\"`\n\n\tBody *ModifyRecordOpenapiResponseBody `json:\"body,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_openapi_response_body.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype ModifyRecordOpenapiResponseBodyTypeEnum string\n\n// List of Type\nconst (\n\tModifyRecordOpenapiResponseBodyTypeEnumA      ModifyRecordOpenapiResponseBodyTypeEnum = \"A\"\n\tModifyRecordOpenapiResponseBodyTypeEnumAaaa   ModifyRecordOpenapiResponseBodyTypeEnum = \"AAAA\"\n\tModifyRecordOpenapiResponseBodyTypeEnumCname  ModifyRecordOpenapiResponseBodyTypeEnum = \"CNAME\"\n\tModifyRecordOpenapiResponseBodyTypeEnumMx     ModifyRecordOpenapiResponseBodyTypeEnum = \"MX\"\n\tModifyRecordOpenapiResponseBodyTypeEnumTxt    ModifyRecordOpenapiResponseBodyTypeEnum = \"TXT\"\n\tModifyRecordOpenapiResponseBodyTypeEnumNs     ModifyRecordOpenapiResponseBodyTypeEnum = \"NS\"\n\tModifyRecordOpenapiResponseBodyTypeEnumSpf    ModifyRecordOpenapiResponseBodyTypeEnum = \"SPF\"\n\tModifyRecordOpenapiResponseBodyTypeEnumSrv    ModifyRecordOpenapiResponseBodyTypeEnum = \"SRV\"\n\tModifyRecordOpenapiResponseBodyTypeEnumCaa    ModifyRecordOpenapiResponseBodyTypeEnum = \"CAA\"\n\tModifyRecordOpenapiResponseBodyTypeEnumCmauth ModifyRecordOpenapiResponseBodyTypeEnum = \"CMAUTH\"\n)\n\ntype ModifyRecordOpenapiResponseBodyTimedStatusEnum string\n\n// List of TimedStatus\nconst (\n\tModifyRecordOpenapiResponseBodyTimedStatusEnumDisabled ModifyRecordOpenapiResponseBodyTimedStatusEnum = \"DISABLED\"\n\tModifyRecordOpenapiResponseBodyTimedStatusEnumEnabled  ModifyRecordOpenapiResponseBodyTimedStatusEnum = \"ENABLED\"\n\tModifyRecordOpenapiResponseBodyTimedStatusEnumTimed    ModifyRecordOpenapiResponseBodyTimedStatusEnum = \"TIMED\"\n)\n\ntype ModifyRecordOpenapiResponseBodyStateEnum string\n\n// List of State\nconst (\n\tModifyRecordOpenapiResponseBodyStateEnumDisabled ModifyRecordOpenapiResponseBodyStateEnum = \"DISABLED\"\n\tModifyRecordOpenapiResponseBodyStateEnumEnabled  ModifyRecordOpenapiResponseBodyStateEnum = \"ENABLED\"\n)\n\ntype ModifyRecordOpenapiResponseBody struct {\n\t// 主机头\n\tRr string `json:\"rr,omitempty\"`\n\n\t// 修改时间\n\tModifiedTime string `json:\"modifiedTime,omitempty\"`\n\n\t// 线路中文名\n\tLineZh string `json:\"lineZh,omitempty\"`\n\n\t// 备注\n\tDescription string `json:\"description,omitempty\"`\n\n\t// 线路ID\n\tLineId string `json:\"lineId,omitempty\"`\n\n\t// 权重值\n\tWeight *int32 `json:\"weight,omitempty\"`\n\n\t// MX优先级\n\tMxPri *int32 `json:\"mxPri,omitempty\"`\n\n\t// 记录类型\n\tType ModifyRecordOpenapiResponseBodyTypeEnum `json:\"type,omitempty\"`\n\n\t// 缓存的生命周期\n\tTtl *int32 `json:\"ttl,omitempty\"`\n\n\t// 标签\n\tTags *[]ModifyRecordOpenapiResponseTags `json:\"tags,omitempty\"`\n\n\t// 解析记录ID\n\tRecordId string `json:\"recordId,omitempty\"`\n\n\t// 定时状态\n\tTimedStatus ModifyRecordOpenapiResponseBodyTimedStatusEnum `json:\"timedStatus,omitempty\"`\n\n\t// 域名名称\n\tDomainName string `json:\"domainName,omitempty\"`\n\n\t// 线路英文名\n\tLineEn string `json:\"lineEn,omitempty\"`\n\n\t// 状态\n\tState ModifyRecordOpenapiResponseBodyStateEnum `json:\"state,omitempty\"`\n\n\t// 记录值\n\tValue string `json:\"value,omitempty\"`\n\n\t// 定时发布时间\n\tPubdate string `json:\"pubdate,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_openapi_response_tags.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype ModifyRecordOpenapiResponseTags struct {\n\t// 标签ID\n\tTagId string `json:\"tagId,omitempty\"`\n\n\t// 标签名称\n\tValue string `json:\"value,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_request.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype ModifyRecordRequest struct {\n\tModifyRecordBody *ModifyRecordBody `json:\"modifyRecordBody,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_response.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype ModifyRecordResponseStateEnum string\n\n// List of State\nconst (\n\tModifyRecordResponseStateEnumError     ModifyRecordResponseStateEnum = \"ERROR\"\n\tModifyRecordResponseStateEnumException ModifyRecordResponseStateEnum = \"EXCEPTION\"\n\tModifyRecordResponseStateEnumForbidden ModifyRecordResponseStateEnum = \"FORBIDDEN\"\n\tModifyRecordResponseStateEnumOk        ModifyRecordResponseStateEnum = \"OK\"\n)\n\ntype ModifyRecordResponse struct {\n\tRequestId string `json:\"requestId,omitempty\"`\n\n\tErrorMessage string `json:\"errorMessage,omitempty\"`\n\n\tErrorCode string `json:\"errorCode,omitempty\"`\n\n\tState ModifyRecordResponseStateEnum `json:\"state,omitempty\"`\n\n\tBody *ModifyRecordResponseBody `json:\"body,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_response_body.go",
    "content": "// @Title  Golang SDK Client\n// @Description  This code is auto generated\n// @Author  Ecloud SDK\n\npackage model\n\ntype ModifyRecordResponseBodyTypeEnum string\n\n// List of Type\nconst (\n\tModifyRecordResponseBodyTypeEnumA      ModifyRecordResponseBodyTypeEnum = \"A\"\n\tModifyRecordResponseBodyTypeEnumAaaa   ModifyRecordResponseBodyTypeEnum = \"AAAA\"\n\tModifyRecordResponseBodyTypeEnumCaa    ModifyRecordResponseBodyTypeEnum = \"CAA\"\n\tModifyRecordResponseBodyTypeEnumCmauth ModifyRecordResponseBodyTypeEnum = \"CMAUTH\"\n\tModifyRecordResponseBodyTypeEnumCname  ModifyRecordResponseBodyTypeEnum = \"CNAME\"\n\tModifyRecordResponseBodyTypeEnumMx     ModifyRecordResponseBodyTypeEnum = \"MX\"\n\tModifyRecordResponseBodyTypeEnumNs     ModifyRecordResponseBodyTypeEnum = \"NS\"\n\tModifyRecordResponseBodyTypeEnumPtr    ModifyRecordResponseBodyTypeEnum = \"PTR\"\n\tModifyRecordResponseBodyTypeEnumRp     ModifyRecordResponseBodyTypeEnum = \"RP\"\n\tModifyRecordResponseBodyTypeEnumSpf    ModifyRecordResponseBodyTypeEnum = \"SPF\"\n\tModifyRecordResponseBodyTypeEnumSrv    ModifyRecordResponseBodyTypeEnum = \"SRV\"\n\tModifyRecordResponseBodyTypeEnumTxt    ModifyRecordResponseBodyTypeEnum = \"TXT\"\n\tModifyRecordResponseBodyTypeEnumUrl    ModifyRecordResponseBodyTypeEnum = \"URL\"\n)\n\ntype ModifyRecordResponseBodyStateEnum string\n\n// List of State\nconst (\n\tModifyRecordResponseBodyStateEnumDisabled ModifyRecordResponseBodyStateEnum = \"DISABLED\"\n\tModifyRecordResponseBodyStateEnumEnabled  ModifyRecordResponseBodyStateEnum = \"ENABLED\"\n)\n\ntype ModifyRecordResponseBody struct {\n\t// 主机头\n\tRr string `json:\"rr,omitempty\"`\n\n\t// 修改时间\n\tModifiedTime string `json:\"modifiedTime,omitempty\"`\n\n\t// 线路中文名\n\tLineZh string `json:\"lineZh,omitempty\"`\n\n\t// 备注\n\tDescription string `json:\"description,omitempty\"`\n\n\t// 线路ID\n\tLineId string `json:\"lineId,omitempty\"`\n\n\t// 权重值\n\tWeight *int32 `json:\"weight,omitempty\"`\n\n\t// MX优先级\n\tMxPri *int32 `json:\"mxPri,omitempty\"`\n\n\t// 记录类型\n\tType ModifyRecordResponseBodyTypeEnum `json:\"type,omitempty\"`\n\n\t// 缓存的生命周期\n\tTtl *int32 `json:\"ttl,omitempty\"`\n\n\t// 解析记录ID\n\tRecordId string `json:\"recordId,omitempty\"`\n\n\t// 域名名称\n\tDomainName string `json:\"domainName,omitempty\"`\n\n\t// 线路英文名\n\tLineEn string `json:\"lineEn,omitempty\"`\n\n\t// 状态\n\tState ModifyRecordResponseBodyStateEnum `json:\"state,omitempty\"`\n\n\t// 记录值\n\tValue string `json:\"value,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0/api_client.go",
    "content": "package ecloudsdkcore\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode/utf8\"\n\n\t\"gitlab.ecloud.com/ecloud/ecloudsdkcore/config\"\n)\n\nvar (\n\tjsonCheck = regexp.MustCompile(\"(?i:(?:application|text)/json)\")\n\txmlCheck  = regexp.MustCompile(\"(?i:(?:application|text)/xml)\")\n)\n\n// APIClient manages communication\n// In most cases there should be only one, shared, APIClient.\ntype APIClient struct {\n\tcfg    *Configuration\n\tcommon service\n}\n\ntype service struct {\n\tclient *APIClient\n}\n\ntype HttpRequestPosition string\n\nconst (\n\tBODY   HttpRequestPosition = \"Body\"\n\tQUERY  HttpRequestPosition = \"Query\"\n\tPATH   HttpRequestPosition = \"Path\"\n\tHEADER HttpRequestPosition = \"Header\"\n)\n\nconst (\n\tSdkPortalUrl        = \"/op-apim-portal/apim/request/sdk\"\n\tSdkPortalGatewayUrl = \"/api/query/openapi/apim/request/sdk\"\n)\n\n// NewAPIClient creates a new API client.\nfunc NewAPIClient() *APIClient {\n\tcfg := NewConfiguration()\n\tif cfg.HTTPClient == nil {\n\t\tcfg.HTTPClient = http.DefaultClient\n\t}\n\tc := &APIClient{}\n\tc.cfg = cfg\n\tc.common.client = c\n\treturn c\n}\n\n// atoi string to int\nfunc atoi(in string) (int, error) {\n\treturn strconv.Atoi(in)\n}\n\n// selectHeaderContentType select a content type from the available list.\nfunc selectHeaderContentType(contentTypes []string) string {\n\tif len(contentTypes) == 0 {\n\t\treturn \"\"\n\t}\n\tif contains(contentTypes, \"application/json\") {\n\t\treturn \"application/json\"\n\t}\n\treturn contentTypes[0]\n}\n\n// selectHeaderAccept join all accept types and return\nfunc selectHeaderAccept(accepts []string) string {\n\tif len(accepts) == 0 {\n\t\treturn \"\"\n\t}\n\n\tif contains(accepts, \"application/json\") {\n\t\treturn \"application/json\"\n\t}\n\n\treturn strings.Join(accepts, \",\")\n}\n\n// contains is a case insenstive match, finding needle in a haystack\nfunc contains(haystack []string, needle string) bool {\n\tfor _, a := range haystack {\n\t\tif strings.ToLower(a) == strings.ToLower(needle) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Verify optional parameters are of the correct type.\nfunc typeCheckParameter(obj interface{}, expected string, name string) error {\n\tif obj == nil {\n\t\treturn nil\n\t}\n\tif reflect.TypeOf(obj).String() != expected {\n\t\treturn fmt.Errorf(\"Expected %s to be of type %s but received %s.\", name, expected, reflect.TypeOf(obj).String())\n\t}\n\treturn nil\n}\n\n// parameterToString convert interface{} parameters to string, using a delimiter if format is provided.\nfunc parameterToString(obj interface{}, collectionFormat string, request HttpRequest) (*http.Request, string) {\n\tvar delimiter string\n\n\tswitch collectionFormat {\n\tcase \"pipes\":\n\t\tdelimiter = \"|\"\n\tcase \"ssv\":\n\t\tdelimiter = \" \"\n\tcase \"tsv\":\n\t\tdelimiter = \"\\t\"\n\tcase \"csv\":\n\t\tdelimiter = \",\"\n\t}\n\n\tif reflect.TypeOf(obj).Kind() == reflect.Slice {\n\t\treturn nil, strings.Trim(strings.Replace(fmt.Sprint(obj), \" \", delimiter, -1), \"[]\")\n\t}\n\n\treturn nil, fmt.Sprintf(\"%v\", obj)\n}\n\n// Excute entry for http call\nfunc (c *APIClient) Excute(httpRequest *HttpRequest, config *config.Config, returnType interface{}) (*http.Response, error) {\n\thttpRequest = buildHttpRequest(httpRequest, config)\n\trequest := buildCall(httpRequest)\n\thttpResponse, err := c.callAPI(request)\n\tif err != nil || httpResponse == nil {\n\t\treturn nil, err\n\t}\n\n\tresponseBody, err := ioutil.ReadAll(httpResponse.Body)\n\thttpResponse.Body.Close()\n\tif err != nil {\n\t\treturn httpResponse, err\n\t}\n\n\tif httpResponse.StatusCode < 300 {\n\t\t// If we succeed, return the data, otherwise pass on to decode error.\n\t\terr = c.decode(&returnType, responseBody, httpResponse.Header.Get(\"Content-Type\"))\n\t\tif err != nil {\n\t\t\treturn httpResponse, fmt.Errorf(\"%w, response body is: %s\", err, string(responseBody))\n\t\t}\n\t\treturn httpResponse, nil\n\t}\n\n\tif httpResponse.StatusCode >= 300 {\n\t\tnewErr := GenericResponseError{\n\t\t\tbody:  responseBody,\n\t\t\terror: httpResponse.Status,\n\t\t}\n\t\treturn httpResponse, newErr\n\t}\n\treturn httpResponse, err\n}\n\n// callAPI do the request.\nfunc (c *APIClient) callAPI(request *http.Request) (*http.Response, error) {\n\treturn c.cfg.HTTPClient.Do(request)\n}\n\n// ChangeBasePath Change base path to allow switching to mocks\nfunc (c *APIClient) ChangeBasePath(path string) {\n\tc.cfg.BasePath = path\n}\n\n// buildHttpRequest build the request\nfunc buildHttpRequest(httpRequest *HttpRequest, config *config.Config) *HttpRequest {\n\topenApiRequest := &OpenApiRequest{\n\t\tAccessKey:  config.AccessKey,\n\t\tSecretKey:  config.SecretKey,\n\t\tPoolId:     config.PoolId,\n\t\tApi:        httpRequest.Action,\n\t\tProduct:    httpRequest.Product,\n\t\tVersion:    httpRequest.Version,\n\t\tSdkVersion: httpRequest.SdkVersion,\n\t\tLanguage:   \"Golang\",\n\t}\n\tif httpRequest.Body != nil {\n\t\treqType := reflect.TypeOf(httpRequest.Body)\n\t\tif reqType.Kind() == reflect.Ptr {\n\t\t\treqType = reqType.Elem()\n\t\t}\n\t\tv := reflect.ValueOf(httpRequest.Body)\n\t\tif v.Kind() == reflect.Ptr {\n\t\t\tv = v.Elem()\n\t\t}\n\t\tflag := false\n\t\tfor i := 0; i < reqType.NumField(); i++ {\n\t\t\tfieldType := reqType.Field(i)\n\t\t\tvalue := v.FieldByName(fieldType.Name)\n\t\t\tif value.Kind() == reflect.Ptr {\n\t\t\t\tif value.IsNil() {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvalue = value.Elem()\n\n\t\t\t}\n\t\t\tpropertyType := fieldType.Type\n\t\t\tif propertyType.Kind() == reflect.Ptr {\n\t\t\t\tpropertyType = propertyType.Elem()\n\t\t\t}\n\n\t\t\t_, flag = propertyType.FieldByName(string(BODY))\n\t\t\tif flag {\n\t\t\t\topenApiRequest.BodyParameter = value.Interface()\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t_, flag = propertyType.FieldByName(string(HEADER))\n\t\t\tif flag {\n\t\t\t\topenApiRequest.HeaderParameter = structToMap(value.Interface())\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t_, flag = propertyType.FieldByName(string(QUERY))\n\t\t\tif flag {\n\t\t\t\topenApiRequest.QueryParameter = structToMap(value.Interface())\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t_, flag = propertyType.FieldByName(string(PATH))\n\t\t\tif flag {\n\t\t\t\topenApiRequest.PathParameter = structToMap(value.Interface())\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}\n\theaders := make(map[string]interface{})\n\tif httpRequest.HeaderParams != nil {\n\t\tif openApiRequest.HeaderParameter == nil {\n\t\t\theaders = httpRequest.HeaderParams\n\t\t} else {\n\t\t\theaders = mergeMap(openApiRequest.HeaderParameter, httpRequest.HeaderParams)\n\t\t}\n\t\topenApiRequest.HeaderParameter = headers\n\t}\n\thttpRequest.Body = openApiRequest\n\treturn httpRequest\n}\n\n// mergeMap merge the two map results\nfunc mergeMap(mObj ...map[string]interface{}) map[string]interface{} {\n\tnewMap := map[string]interface{}{}\n\tfor _, m := range mObj {\n\t\tfor k, v := range m {\n\t\t\tnewMap[k] = v\n\t\t}\n\t}\n\treturn newMap\n}\n\n// structToMap struct convert to map\nfunc structToMap(value interface{}) map[string]interface{} {\n\tdata, _ := json.Marshal(value)\n\tresult := make(map[string]interface{})\n\tjson.Unmarshal(data, &result)\n\treturn result\n}\n\nfunc buildCall(httpRequest *HttpRequest) (request *http.Request) {\n\turl := \"\"\n\tif len(httpRequest.Url) > 0 {\n\t\turl = httpRequest.Url + SdkPortalUrl\n\t} else {\n\t\turl = httpRequest.DefaultUrl + SdkPortalGatewayUrl\n\t}\n\trequest, _ = prepareRequest(url, \"POST\", httpRequest.Body)\n\treturn request\n}\n\n// prepareRequest build the request\nfunc prepareRequest(path string, method string,\n\tpostBody interface{},\n) (httpRequest *http.Request, err error) {\n\tvar body *bytes.Buffer\n\n\t// Detect postBody type and post.\n\tif postBody != nil {\n\t\tcontentType := detectContentType(postBody)\n\t\tbody, err = setBody(postBody, contentType)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// Setup path and query parameters\n\turl, err := url.Parse(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Generate a new request\n\tif body != nil {\n\t\thttpRequest, err = http.NewRequest(method, url.String(), body)\n\t} else {\n\t\thttpRequest, err = http.NewRequest(method, url.String(), nil)\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// add default header parameters\n\thttpRequest.Header.Add(\"Content-Type\", \"application/json\")\n\treturn httpRequest, nil\n}\n\nfunc (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) {\n\tif strings.Contains(contentType, \"application/xml\") {\n\t\tif err = xml.Unmarshal(b, v); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t} else if strings.Contains(contentType, \"application/json\") {\n\t\tplatformResponse := &APIPlatformResponse{}\n\t\tif err = json.Unmarshal(b, platformResponse); err != nil {\n\t\t\tnewErr := GenericResponseError{\n\t\t\t\tbody:  b,\n\t\t\t\terror: err.Error(),\n\t\t\t}\n\t\t\treturn newErr\n\t\t}\n\t\tplatformResponseBodyBytes, _ := json.Marshal(platformResponse.Body)\n\t\tplatformResponseBody := &APIPlatformResponseBody{}\n\t\tif err = json.Unmarshal(platformResponseBodyBytes, platformResponseBody); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t/*\n\t\t\t找到两层指针指向的元素\n\t\t*/\n\t\tvalue := reflect.ValueOf(v).Elem().Elem()\n\n\t\tif !value.IsNil() {\n\t\t\tstructValue := value.Elem()\n\t\t\tif structValue.NumField() == 1 && structValue.Field(0).Kind() == reflect.String {\n\t\t\t\tn := len(platformResponseBody.ResponseBody)\n\t\t\t\tstructValue.Field(0).SetString(platformResponseBody.ResponseBody[1 : n-1])\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\tif err = json.Unmarshal([]byte(platformResponseBody.ResponseBody), v); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\treturn errors.New(\"undefined response type\")\n}\n\n// Add a file to the multipart request\nfunc addFile(w *multipart.Writer, fieldName, path string) error {\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\n\tpart, err := w.CreateFormFile(fieldName, filepath.Base(path))\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = io.Copy(part, file)\n\n\treturn err\n}\n\n// Prevent trying to import \"fmt\"\nfunc reportError(format string, a ...interface{}) error {\n\treturn fmt.Errorf(format, a...)\n}\n\n// Set request body from an interface{}\nfunc setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) {\n\tif bodyBuf == nil {\n\t\tbodyBuf = &bytes.Buffer{}\n\t}\n\tif reader, ok := body.(io.Reader); ok {\n\t\t_, err = bodyBuf.ReadFrom(reader)\n\t} else if b, ok := body.([]byte); ok {\n\t\t_, err = bodyBuf.Write(b)\n\t} else if s, ok := body.(string); ok {\n\t\t_, err = bodyBuf.WriteString(s)\n\t} else if s, ok := body.(*string); ok {\n\t\t_, err = bodyBuf.WriteString(*s)\n\t} else if jsonCheck.MatchString(contentType) {\n\t\terr = json.NewEncoder(bodyBuf).Encode(body)\n\t} else if xmlCheck.MatchString(contentType) {\n\t\txml.NewEncoder(bodyBuf).Encode(body)\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif bodyBuf.Len() == 0 {\n\t\terr = fmt.Errorf(\"Invalid body type %s\\n\", contentType)\n\t\treturn nil, err\n\t}\n\treturn bodyBuf, nil\n}\n\n// detectContentType method is used to figure out `Request.Body` content type for request header\nfunc detectContentType(body interface{}) string {\n\tcontentType := \"text/plain; charset=utf-8\"\n\tkind := reflect.TypeOf(body).Kind()\n\n\tswitch kind {\n\tcase reflect.Struct, reflect.Map, reflect.Ptr:\n\t\tcontentType = \"application/json; charset=utf-8\"\n\tcase reflect.String:\n\t\tcontentType = \"text/plain; charset=utf-8\"\n\tdefault:\n\t\tif b, ok := body.([]byte); ok {\n\t\t\tcontentType = http.DetectContentType(b)\n\t\t} else if kind == reflect.Slice {\n\t\t\tcontentType = \"application/json; charset=utf-8\"\n\t\t}\n\t}\n\n\treturn contentType\n}\n\ntype cacheControl map[string]string\n\nfunc parseCacheControl(headers http.Header) cacheControl {\n\tcc := cacheControl{}\n\tccHeader := headers.Get(\"Cache-Control\")\n\tfor _, part := range strings.Split(ccHeader, \",\") {\n\t\tpart = strings.Trim(part, \" \")\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif strings.ContainsRune(part, '=') {\n\t\t\tkeyval := strings.Split(part, \"=\")\n\t\t\tcc[strings.Trim(keyval[0], \" \")] = strings.Trim(keyval[1], \",\")\n\t\t} else {\n\t\t\tcc[part] = \"\"\n\t\t}\n\t}\n\treturn cc\n}\n\n// CacheExpires helper function to determine remaining time before repeating a request.\nfunc CacheExpires(r *http.Response) time.Time {\n\t// Figure out when the cache expires.\n\tvar expires time.Time\n\tnow, err := time.Parse(time.RFC1123, r.Header.Get(\"date\"))\n\tif err != nil {\n\t\treturn time.Now()\n\t}\n\trespCacheControl := parseCacheControl(r.Header)\n\n\tif maxAge, ok := respCacheControl[\"max-age\"]; ok {\n\t\tlifetime, err := time.ParseDuration(maxAge + \"s\")\n\t\tif err != nil {\n\t\t\texpires = now\n\t\t}\n\t\texpires = now.Add(lifetime)\n\t} else {\n\t\texpiresHeader := r.Header.Get(\"Expires\")\n\t\tif expiresHeader != \"\" {\n\t\t\texpires, err = time.Parse(time.RFC1123, expiresHeader)\n\t\t\tif err != nil {\n\t\t\t\texpires = now\n\t\t\t}\n\t\t}\n\t}\n\treturn expires\n}\n\nfunc strlen(s string) int {\n\treturn utf8.RuneCountInString(s)\n}\n\n// GenericResponseError Provides access to the body, error and model on returned errors.\ntype GenericResponseError struct {\n\tbody  []byte\n\terror string\n\tmodel interface{}\n}\n\n// Error returns non-empty string if there was an error.\nfunc (e GenericResponseError) Error() string {\n\treturn e.error\n}\n\n// Body returns the raw bytes of the response\nfunc (e GenericResponseError) Body() []byte {\n\treturn e.body\n}\n\n// Model returns the unpacked model of the error\nfunc (e GenericResponseError) Model() interface{} {\n\treturn e.model\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0/api_response.go",
    "content": "package ecloudsdkcore\n\nimport (\n\t\"net/http\"\n)\n\ntype ReturnState string\n\nconst (\n\tOK        ReturnState = \"OK\"\n\tERROR     ReturnState = \"ERROR\"\n\tEXCEPTION ReturnState = \"EXCEPTION\"\n\tALARM     ReturnState = \"ALARM\"\n\tFORBIDDEN ReturnState = \"FORBIDDEN\"\n)\n\ntype APIResponse struct {\n\t*http.Response `json:\"-\"`\n\tMessage        string `json:\"message,omitempty\"`\n\t// Operation is the name of the swagger operation.\n\tOperation string `json:\"operation,omitempty\"`\n\t// RequestURL is the request URL. This value is always available, even if the\n\t// embedded *http.Response is nil.\n\tRequestURL string `json:\"url,omitempty\"`\n\t// Method is the HTTP method used for the request.  This value is always\n\t// available, even if the embedded *http.Response is nil.\n\tMethod string `json:\"method,omitempty\"`\n\t// Payload holds the contents of the response body (which may be nil or empty).\n\t// This is provided here as the raw response.Body() reader will have already\n\t// been drained.\n\tPayload []byte `json:\"-\"`\n}\n\ntype APIPlatformResponse struct {\n\tRequestId    string      `json:\"requestId,omitempty\"`\n\tState        ReturnState `json:\"state,omitempty\"`\n\tBody         interface{} `json:\"body,omitempty\"`\n\tErrorCode    string      `json:\"errorCode,omitempty\"`\n\tErrorParams  []string    `json:\"errorParams,omitempty\"`\n\tErrorMessage string      `json:\"errorMessage,omitempty\"`\n}\n\ntype APIPlatformResponseBody struct {\n\t// TimeConsuming   int64                  `json:\"timeConsuming,omitempty\"`\n\tResponseBody    string                 `json:\"responseBody,omitempty\"`\n\tRequestHeader   map[string]interface{} `json:\"requestHeader,omitempty\"`\n\tResponseHeader  map[string]interface{} `json:\"responseHeader,omitempty\"`\n\tResponseMessage string                 `json:\"responseMessage,omitempty\"`\n\tStatusCode      int                    `json:\"statusCode,omitempty\"`\n\tHttpMethod      string                 `json:\"httpMethod,omitempty\"`\n\tRequestUrl      string                 `json:\"requestUrl,omitempty\"`\n}\n\nfunc NewAPIResponse(r *http.Response) *APIResponse {\n\tresponse := &APIResponse{Response: r}\n\treturn response\n}\n\nfunc NewAPIResponseWithError(errorMessage string) *APIResponse {\n\tresponse := &APIResponse{Message: errorMessage}\n\treturn response\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0/config/config.go",
    "content": "package config\n\ntype Config struct {\n\tAccessKey      string `json:\"accessKey,string\"`\n\tSecretKey      string `json:\"secretKey,string\"`\n\tPoolId         string `json:\"poolId,string\"`\n\tReadTimeOut    int    `json:\"readTimeOut,int\"`\n\tConnectTimeout int    `json:\"connectTimeout,int\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0/configuration.go",
    "content": "package ecloudsdkcore\n\nimport (\n\t\"net/http\"\n)\n\ntype APIKey struct {\n\tKey    string\n\tPrefix string\n}\n\ntype Configuration struct {\n\tBasePath      string            `json:\"basePath,omitempty\"`\n\tHost          string            `json:\"host,omitempty\"`\n\tScheme        string            `json:\"scheme,omitempty\"`\n\tDefaultHeader map[string]string `json:\"defaultHeader,omitempty\"`\n\tUserAgent     string            `json:\"userAgent,omitempty\"`\n\tHTTPClient    *http.Client\n}\n\nfunc NewConfiguration() *Configuration {\n\tcfg := &Configuration{\n\t\tBasePath:      \"https://ecloud.10086.cn/\",\n\t\tDefaultHeader: make(map[string]string),\n\t\tUserAgent:     \"Ecloud-SDK/1.0.0/go\",\n\t}\n\treturn cfg\n}\n\nfunc (c *Configuration) AddDefaultHeader(key string, value string) {\n\tc.DefaultHeader[key] = value\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0/go.mod",
    "content": "module gitlab.ecloud.com/ecloud/ecloudsdkcore\n\ngo 1.14\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0/http_request.go",
    "content": "package ecloudsdkcore\n\ntype HttpRequest struct {\n\tUrl          string                 `json:\"url,omitempty\"`\n\tDefaultUrl   string                 `json:\"defaultUrl,omitempty\"`\n\tMethod       string                 `json:\"method,omitempty\"`\n\tAction       string                 `json:\"action,omitempty\"`\n\tProduct      string                 `json:\"product,omitempty\"`\n\tVersion      string                 `json:\"version,omitempty\"`\n\tSdkVersion   string                 `json:\"sdkVersion,omitempty\"`\n\tBody         interface{}            `json:\"body,omitempty\"`\n\tPathParams   map[string]interface{} `json:\"pathParams,omitempty\"`\n\tQueryParams  map[string]interface{} `json:\"queryParams,omitempty\"`\n\tHeaderParams map[string]interface{} `json:\"headerParams,omitempty\"`\n}\n\nfunc NewDefaultHttpRequest() *HttpRequest {\n\treturn &HttpRequest{\n\t\tDefaultUrl: \"https://ecloud.10086.cn\",\n\t\tMethod:     \"POST\",\n\t}\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0/open_api_request.go",
    "content": "package ecloudsdkcore\n\ntype OpenApiRequest struct {\n\tProduct         string                 `json:\"product,omitempty\"`\n\tVersion         string                 `json:\"version,omitempty\"`\n\tSdkVersion      string                 `json:\"sdkVersion,omitempty\"`\n\tLanguage        string                 `json:\"language,omitempty\"`\n\tApi             string                 `json:\"api,omitempty\"`\n\tPoolId          string                 `json:\"poolId,omitempty\"`\n\tHeaderParameter map[string]interface{} `json:\"headerParameter,omitempty\"`\n\tPathParameter   map[string]interface{} `json:\"pathParameter,omitempty\"`\n\tQueryParameter  map[string]interface{} `json:\"queryParameter,omitempty\"`\n\tBodyParameter   interface{}            `json:\"bodyParameter,omitempty\"`\n\tAccessKey       string                 `json:\"accessKey,omitempty\"`\n\tSecretKey       string                 `json:\"secretKey,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0/position/http_position.go",
    "content": "package position\n\ntype Body struct{}\n\ntype Query struct{}\n\ntype Path struct{}\n\ntype Header struct{}\n"
  },
  {
    "path": "pkg/logging/handler.go",
    "content": "package logging\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"sync\"\n)\n\ntype HookHandlerOptions struct {\n\tLevel     slog.Leveler\n\tWriteFunc func(ctx context.Context, record Record) error\n}\n\nvar _ slog.Handler = (*HookHandler)(nil)\n\ntype HookHandler struct {\n\tmutex   *sync.Mutex\n\tparent  *HookHandler\n\toptions *HookHandlerOptions\n\tgroup   string\n\tattrs   []slog.Attr\n}\n\nfunc NewHookHandler(opts *HookHandlerOptions) *HookHandler {\n\tif opts == nil {\n\t\topts = &HookHandlerOptions{}\n\t}\n\n\th := &HookHandler{\n\t\tmutex:   &sync.Mutex{},\n\t\toptions: opts,\n\t}\n\n\tif h.options.WriteFunc == nil {\n\t\tpanic(\"the `options.WriteFunc` is nil\")\n\t}\n\n\tif h.options.Level == nil {\n\t\th.options.Level = slog.LevelInfo\n\t}\n\n\treturn h\n}\n\nfunc (h *HookHandler) Enabled(ctx context.Context, level slog.Level) bool {\n\treturn level >= h.options.Level.Level()\n}\n\nfunc (h *HookHandler) WithGroup(name string) slog.Handler {\n\tif name == \"\" {\n\t\treturn h\n\t}\n\n\treturn &HookHandler{\n\t\tparent:  h,\n\t\tmutex:   h.mutex,\n\t\toptions: h.options,\n\t\tgroup:   name,\n\t}\n}\n\nfunc (h *HookHandler) WithAttrs(attrs []slog.Attr) slog.Handler {\n\tif len(attrs) == 0 {\n\t\treturn h\n\t}\n\n\treturn &HookHandler{\n\t\tparent:  h,\n\t\tmutex:   h.mutex,\n\t\toptions: h.options,\n\t\tattrs:   attrs,\n\t}\n}\n\nfunc (h *HookHandler) Handle(ctx context.Context, r slog.Record) error {\n\tif h.group != \"\" {\n\t\th.mutex.Lock()\n\t\tattrs := make([]any, 0, len(h.attrs)+r.NumAttrs())\n\t\tfor _, a := range h.attrs {\n\t\t\tattrs = append(attrs, a)\n\t\t}\n\t\th.mutex.Unlock()\n\n\t\tr.Attrs(func(a slog.Attr) bool {\n\t\t\tattrs = append(attrs, a)\n\t\t\treturn true\n\t\t})\n\n\t\tr = slog.NewRecord(r.Time, r.Level, r.Message, r.PC)\n\t\tr.AddAttrs(slog.Group(h.group, attrs...))\n\t} else if len(h.attrs) > 0 {\n\t\tr = r.Clone()\n\n\t\th.mutex.Lock()\n\t\tr.AddAttrs(h.attrs...)\n\t\th.mutex.Unlock()\n\t}\n\n\tif h.parent != nil {\n\t\treturn h.parent.Handle(ctx, r)\n\t}\n\n\tif err := h.writeRecord(ctx, Record{Record: r}); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (h *HookHandler) SetLevel(level slog.Level) {\n\th.mutex.Lock()\n\th.options.Level = level\n\th.mutex.Unlock()\n}\n\nfunc (h *HookHandler) writeRecord(ctx context.Context, r Record) error {\n\tif h.parent != nil {\n\t\treturn h.parent.writeRecord(ctx, r)\n\t}\n\n\treturn h.options.WriteFunc(ctx, r)\n}\n"
  },
  {
    "path": "pkg/logging/record.go",
    "content": "package logging\n\nimport (\n\t\"log/slog\"\n\n\ttypes \"github.com/pocketbase/pocketbase/tools/types\"\n)\n\ntype Record struct {\n\tslog.Record\n}\n\nfunc (r Record) Data() types.JSONMap[any] {\n\tdata := make(map[string]any, r.NumAttrs())\n\n\tr.Attrs(func(a slog.Attr) bool {\n\t\tif err := r.resolveAttr(data, a); err != nil {\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t})\n\n\treturn types.JSONMap[any](data)\n}\n\nfunc (r Record) resolveAttr(data map[string]any, attr slog.Attr) error {\n\tattr.Value = attr.Value.Resolve()\n\n\tif attr.Equal(slog.Attr{}) {\n\t\treturn nil\n\t}\n\n\tswitch attr.Value.Kind() {\n\tcase slog.KindGroup:\n\t\t{\n\t\t\tattrs := attr.Value.Group()\n\t\t\tif len(attrs) == 0 {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tgroupData := make(map[string]any, len(attrs))\n\n\t\t\tfor _, subAttr := range attrs {\n\t\t\t\tr.resolveAttr(groupData, subAttr)\n\t\t\t}\n\n\t\t\tif len(groupData) > 0 {\n\t\t\t\tdata[attr.Key] = groupData\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\t{\n\t\t\tswitch v := attr.Value.Any().(type) {\n\t\t\tcase error:\n\t\t\t\tdata[attr.Key] = v.Error()\n\t\t\tdefault:\n\t\t\t\tdata[attr.Key] = v\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/api_settings_ssl_update.go",
    "content": "package onepanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype SettingsSSLUpdateRequest struct {\n\tCert        string `json:\"cert\"`\n\tKey         string `json:\"key\"`\n\tSSLType     string `json:\"sslType\"`\n\tSSL         string `json:\"ssl\"`\n\tSSLID       int64  `json:\"sslID\"`\n\tAutoRestart string `json:\"autoRestart\"`\n}\n\ntype SettingsSSLUpdateResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) SettingsSSLUpdate(req *SettingsSSLUpdateRequest) (*SettingsSSLUpdateResponse, error) {\n\treturn c.SettingsSSLUpdateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) SettingsSSLUpdateWithContext(ctx context.Context, req *SettingsSSLUpdateRequest) (*SettingsSSLUpdateResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/settings/ssl/update\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SettingsSSLUpdateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/api_website_get.go",
    "content": "package onepanel\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype WebsiteGetRequest struct {\n\tName     string `json:\"name\"`\n\tType     string `json:\"type\"`\n\tPage     int32  `json:\"page\"`\n\tPageSize int32  `json:\"pageSize\"`\n}\n\ntype WebsiteGetResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tID            int64  `json:\"id\"`\n\t\tAlias         string `json:\"alias\"`\n\t\tPrimaryDomain string `json:\"primaryDomain\"`\n\t\tProtocol      string `json:\"protocol\"`\n\t\tType          string `json:\"type\"`\n\t\tStatus        string `json:\"status\"`\n\t\tSitePath      string `json:\"sitePath\"`\n\t\tRemark        string `json:\"remark\"`\n\t\tDomains       []*struct {\n\t\t\tID        int64  `json:\"id\"`\n\t\t\tDomain    string `json:\"domain\"`\n\t\t\tPort      int32  `json:\"port\"`\n\t\t\tSSL       bool   `json:\"ssl\"`\n\t\t\tUpdatedAt string `json:\"updatedAt\"`\n\t\t\tCreatedAt string `json:\"createdAt\"`\n\t\t} `json:\"domains\"`\n\t\tWebsiteSSLId int64  `json:\"webSiteSSLId\"`\n\t\tUpdatedAt    string `json:\"updatedAt\"`\n\t\tCreatedAt    string `json:\"createdAt\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) WebsiteGet(websiteId int64) (*WebsiteGetResponse, error) {\n\treturn c.WebsiteGetWithContext(context.Background(), websiteId)\n}\n\nfunc (c *Client) WebsiteGetWithContext(ctx context.Context, websiteId int64) (*WebsiteGetResponse, error) {\n\tif websiteId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset websiteId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf(\"/websites/%d\", websiteId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &WebsiteGetResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/api_website_https_get.go",
    "content": "package onepanel\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype WebsiteHttpsGetResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tEnable       bool     `json:\"enable\"`\n\t\tWebsiteSSLID int64    `json:\"websiteSSLId\"`\n\t\tHttpConfig   string   `json:\"httpConfig\"`\n\t\tSSLProtocol  []string `json:\"SSLProtocol\"`\n\t\tAlgorithm    string   `json:\"algorithm\"`\n\t\tHsts         bool     `json:\"hsts\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) WebsiteHttpsGet(websiteId int64) (*WebsiteHttpsGetResponse, error) {\n\treturn c.WebsiteHttpsGetWithContext(context.Background(), websiteId)\n}\n\nfunc (c *Client) WebsiteHttpsGetWithContext(ctx context.Context, websiteId int64) (*WebsiteHttpsGetResponse, error) {\n\tif websiteId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset websiteId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf(\"/websites/%d/https\", websiteId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &WebsiteHttpsGetResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/api_website_https_post.go",
    "content": "package onepanel\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype WebsiteHttpsPostRequest struct {\n\tWebsiteID    int64    `json:\"websiteId\"`\n\tEnable       bool     `json:\"enable\"`\n\tType         string   `json:\"type\"`\n\tWebsiteSSLID int64    `json:\"websiteSSLId\"`\n\tHttpConfig   string   `json:\"httpConfig\"`\n\tSSLProtocol  []string `json:\"SSLProtocol\"`\n\tAlgorithm    string   `json:\"algorithm\"`\n\tHsts         bool     `json:\"hsts\"`\n}\n\ntype WebsiteHttpsPostResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) WebsiteHttpsPost(websiteId int64, req *WebsiteHttpsPostRequest) (*WebsiteHttpsPostResponse, error) {\n\treturn c.WebsiteHttpsPostWithContext(context.Background(), websiteId, req)\n}\n\nfunc (c *Client) WebsiteHttpsPostWithContext(ctx context.Context, websiteId int64, req *WebsiteHttpsPostRequest) (*WebsiteHttpsPostResponse, error) {\n\tif websiteId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset websiteId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf(\"/websites/%d/https\", websiteId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treq.WebsiteID = websiteId\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &WebsiteHttpsPostResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/api_website_search.go",
    "content": "package onepanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype WebsiteSearchRequest struct {\n\tName     string `json:\"name\"`\n\tType     string `json:\"type\"`\n\tOrder    string `json:\"order\"`\n\tOrderBy  string `json:\"orderBy\"`\n\tPage     int32  `json:\"page\"`\n\tPageSize int32  `json:\"pageSize\"`\n}\n\ntype WebsiteSearchResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tItems []*struct {\n\t\t\tID            int64  `json:\"id\"`\n\t\t\tAlias         string `json:\"alias\"`\n\t\t\tPrimaryDomain string `json:\"primaryDomain\"`\n\t\t\tProtocol      string `json:\"protocol\"`\n\t\t\tType          string `json:\"type\"`\n\t\t\tStatus        string `json:\"status\"`\n\t\t\tSitePath      string `json:\"sitePath\"`\n\t\t\tRemark        string `json:\"remark\"`\n\t\t\tSSLStatus     string `json:\"sslStatus\"`\n\t\t\tSSLExpireDate string `json:\"sslExpireDate\"`\n\t\t\tUpdatedAt     string `json:\"updatedAt\"`\n\t\t\tCreatedAt     string `json:\"createdAt\"`\n\t\t} `json:\"items\"`\n\t\tTotal int32 `json:\"total\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) WebsiteSearch(req *WebsiteSearchRequest) (*WebsiteSearchResponse, error) {\n\treturn c.WebsiteSearchWithContext(context.Background(), req)\n}\n\nfunc (c *Client) WebsiteSearchWithContext(ctx context.Context, req *WebsiteSearchRequest) (*WebsiteSearchResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/websites/search\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &WebsiteSearchResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/api_website_ssl_get.go",
    "content": "package onepanel\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype WebsiteSSLGetResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tID            int64  `json:\"id\"`\n\t\tProvider      string `json:\"provider\"`\n\t\tDescription   string `json:\"description\"`\n\t\tPrimaryDomain string `json:\"primaryDomain\"`\n\t\tDomains       string `json:\"domains\"`\n\t\tType          string `json:\"type\"`\n\t\tOrganization  string `json:\"organization\"`\n\t\tStatus        string `json:\"status\"`\n\t\tStartDate     string `json:\"startDate\"`\n\t\tExpireDate    string `json:\"expireDate\"`\n\t\tCreatedAt     string `json:\"createdAt\"`\n\t\tUpdatedAt     string `json:\"updatedAt\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) WebsiteSSLGet(sslId int64) (*WebsiteSSLGetResponse, error) {\n\treturn c.WebsiteSSLGetWithContext(context.Background(), sslId)\n}\n\nfunc (c *Client) WebsiteSSLGetWithContext(ctx context.Context, sslId int64) (*WebsiteSSLGetResponse, error) {\n\tif sslId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset sslId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf(\"/websites/ssl/%d\", sslId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &WebsiteSSLGetResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/api_website_ssl_search.go",
    "content": "package onepanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype WebsiteSSLSearchRequest struct {\n\tDomain   string `json:\"domain\"`\n\tPage     int32  `json:\"page\"`\n\tPageSize int32  `json:\"pageSize\"`\n}\n\ntype WebsiteSSLSearchResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tItems []*struct {\n\t\t\tID          int64  `json:\"id\"`\n\t\t\tPEM         string `json:\"pem\"`\n\t\t\tPrivateKey  string `json:\"privateKey\"`\n\t\t\tDomains     string `json:\"domains\"`\n\t\t\tDescription string `json:\"description\"`\n\t\t\tStatus      string `json:\"status\"`\n\t\t\tUpdatedAt   string `json:\"updatedAt\"`\n\t\t\tCreatedAt   string `json:\"createdAt\"`\n\t\t} `json:\"items\"`\n\t\tTotal int32 `json:\"total\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) WebsiteSSLSearch(req *WebsiteSSLSearchRequest) (*WebsiteSSLSearchResponse, error) {\n\treturn c.WebsiteSSLSearchWithContext(context.Background(), req)\n}\n\nfunc (c *Client) WebsiteSSLSearchWithContext(ctx context.Context, req *WebsiteSSLSearchRequest) (*WebsiteSSLSearchResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/websites/ssl/search\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &WebsiteSSLSearchResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/api_website_ssl_upload.go",
    "content": "package onepanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype WebsiteSSLUploadRequest struct {\n\tSSLID           int64  `json:\"sslID\"`\n\tType            string `json:\"type\"`\n\tCertificate     string `json:\"certificate\"`\n\tCertificatePath string `json:\"certificatePath\"`\n\tPrivateKey      string `json:\"privateKey\"`\n\tPrivateKeyPath  string `json:\"privateKeyPath\"`\n\tDescription     string `json:\"description\"`\n}\n\ntype WebsiteSSLUploadResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) WebsiteSSLUpload(req *WebsiteSSLUploadRequest) (*WebsiteSSLUploadResponse, error) {\n\treturn c.WebsiteSSLUploadWithContext(context.Background(), req)\n}\n\nfunc (c *Client) WebsiteSSLUploadWithContext(ctx context.Context, req *WebsiteSSLUploadRequest) (*WebsiteSSLUploadResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/websites/ssl/upload\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &WebsiteSSLUploadResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/client.go",
    "content": "package onepanel\n\nimport (\n\t\"crypto/md5\"\n\t\"crypto/tls\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl, apiKey string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif apiKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiKey\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")+\"/api/v1\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\ttimestamp := fmt.Sprintf(\"%d\", time.Now().Unix())\n\t\t\ttokenMd5 := md5.Sum([]byte(\"1panel\" + apiKey + timestamp))\n\t\t\ttokenMd5Hex := hex.EncodeToString(tokenMd5[:])\n\t\t\treq.Header.Set(\"1Panel-Timestamp\", timestamp)\n\t\t\treq.Header.Set(\"1Panel-Token\", tokenMd5Hex)\n\n\t\t\treturn nil\n\t\t})\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tcode := res.GetCode(); tcode/100 != 2 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: code='%d', message='%s'\", tcode, res.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/types.go",
    "content": "package onepanel\n\ntype sdkResponse interface {\n\tGetCode() int\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    *int    `json:\"code,omitempty\"`\n\tMessage *string `json:\"message,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() int {\n\tif r.Code == nil {\n\t\treturn 0\n\t}\n\n\treturn *r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/v2/api_core_settings_ssl_update.go",
    "content": "package v2\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype CoreSettingsSSLUpdateRequest struct {\n\tCert        string `json:\"cert\"`\n\tKey         string `json:\"key\"`\n\tSSLType     string `json:\"sslType\"`\n\tSSL         string `json:\"ssl\"`\n\tSSLID       int64  `json:\"sslID\"`\n\tAutoRestart string `json:\"autoRestart\"`\n}\n\ntype CoreSettingsSSLUpdateResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) CoreSettingsSSLUpdate(req *CoreSettingsSSLUpdateRequest) (*CoreSettingsSSLUpdateResponse, error) {\n\treturn c.CoreSettingsSSLUpdateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) CoreSettingsSSLUpdateWithContext(ctx context.Context, req *CoreSettingsSSLUpdateRequest) (*CoreSettingsSSLUpdateResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/core/settings/ssl/update\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &CoreSettingsSSLUpdateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/v2/api_website_get.go",
    "content": "package v2\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype WebsiteGetRequest struct {\n\tName     string `json:\"name\"`\n\tType     string `json:\"type\"`\n\tPage     int32  `json:\"page\"`\n\tPageSize int32  `json:\"pageSize\"`\n}\n\ntype WebsiteGetResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tID            int64  `json:\"id\"`\n\t\tAlias         string `json:\"alias\"`\n\t\tPrimaryDomain string `json:\"primaryDomain\"`\n\t\tProtocol      string `json:\"protocol\"`\n\t\tType          string `json:\"type\"`\n\t\tStatus        string `json:\"status\"`\n\t\tSitePath      string `json:\"sitePath\"`\n\t\tRemark        string `json:\"remark\"`\n\t\tDomains       []*struct {\n\t\t\tID        int64  `json:\"id\"`\n\t\t\tDomain    string `json:\"domain\"`\n\t\t\tPort      int32  `json:\"port\"`\n\t\t\tSSL       bool   `json:\"ssl\"`\n\t\t\tUpdatedAt string `json:\"updatedAt\"`\n\t\t\tCreatedAt string `json:\"createdAt\"`\n\t\t} `json:\"domains,omitempty\"`\n\t\tWebsiteSSLId int64  `json:\"webSiteSSLId\"`\n\t\tUpdatedAt    string `json:\"updatedAt\"`\n\t\tCreatedAt    string `json:\"createdAt\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) WebsiteGet(websiteId int64) (*WebsiteGetResponse, error) {\n\treturn c.WebsiteGetWithContext(context.Background(), websiteId)\n}\n\nfunc (c *Client) WebsiteGetWithContext(ctx context.Context, websiteId int64) (*WebsiteGetResponse, error) {\n\tif websiteId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset websiteId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf(\"/websites/%d\", websiteId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &WebsiteGetResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/v2/api_website_https_get.go",
    "content": "package v2\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype WebsiteHttpsGetResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tEnable       bool     `json:\"enable\"`\n\t\tHttpConfig   string   `json:\"httpConfig\"`\n\t\tWebsiteSSLID int64    `json:\"websiteSSLId\"`\n\t\tSSLProtocol  []string `json:\"SSLProtocol\"`\n\t\tAlgorithm    string   `json:\"algorithm\"`\n\t\tHsts         bool     `json:\"hsts\"`\n\t\tHttp3        bool     `json:\"http3\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) WebsiteHttpsGet(websiteId int64) (*WebsiteHttpsGetResponse, error) {\n\treturn c.WebsiteHttpsGetWithContext(context.Background(), websiteId)\n}\n\nfunc (c *Client) WebsiteHttpsGetWithContext(ctx context.Context, websiteId int64) (*WebsiteHttpsGetResponse, error) {\n\tif websiteId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset websiteId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf(\"/websites/%d/https\", websiteId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &WebsiteHttpsGetResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/v2/api_website_https_post.go",
    "content": "package v2\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype WebsiteHttpsPostRequest struct {\n\tWebsiteID    int64    `json:\"websiteId\"`\n\tEnable       bool     `json:\"enable\"`\n\tType         string   `json:\"type\"`\n\tWebsiteSSLID int64    `json:\"websiteSSLId\"`\n\tHttpConfig   string   `json:\"httpConfig\"`\n\tSSLProtocol  []string `json:\"SSLProtocol\"`\n\tAlgorithm    string   `json:\"algorithm\"`\n\tHsts         bool     `json:\"hsts\"`\n\tHttp3        bool     `json:\"http3\"`\n}\n\ntype WebsiteHttpsPostResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) WebsiteHttpsPost(websiteId int64, req *WebsiteHttpsPostRequest) (*WebsiteHttpsPostResponse, error) {\n\treturn c.WebsiteHttpsPostWithContext(context.Background(), websiteId, req)\n}\n\nfunc (c *Client) WebsiteHttpsPostWithContext(ctx context.Context, websiteId int64, req *WebsiteHttpsPostRequest) (*WebsiteHttpsPostResponse, error) {\n\tif websiteId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset websiteId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf(\"/websites/%d/https\", websiteId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treq.WebsiteID = websiteId\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &WebsiteHttpsPostResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/v2/api_website_search.go",
    "content": "package v2\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype WebsiteSearchRequest struct {\n\tName     string `json:\"name\"`\n\tType     string `json:\"type\"`\n\tOrder    string `json:\"order\"`\n\tOrderBy  string `json:\"orderBy\"`\n\tPage     int32  `json:\"page\"`\n\tPageSize int32  `json:\"pageSize\"`\n}\n\ntype WebsiteSearchResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tItems []*struct {\n\t\t\tID            int64  `json:\"id\"`\n\t\t\tAlias         string `json:\"alias\"`\n\t\t\tPrimaryDomain string `json:\"primaryDomain\"`\n\t\t\tProtocol      string `json:\"protocol\"`\n\t\t\tType          string `json:\"type\"`\n\t\t\tStatus        string `json:\"status\"`\n\t\t\tSitePath      string `json:\"sitePath\"`\n\t\t\tRemark        string `json:\"remark\"`\n\t\t\tSSLStatus     string `json:\"sslStatus\"`\n\t\t\tSSLExpireDate string `json:\"sslExpireDate\"`\n\t\t\tUpdatedAt     string `json:\"updatedAt\"`\n\t\t\tCreatedAt     string `json:\"createdAt\"`\n\t\t} `json:\"items\"`\n\t\tTotal int32 `json:\"total\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) WebsiteSearch(req *WebsiteSearchRequest) (*WebsiteSearchResponse, error) {\n\treturn c.WebsiteSearchWithContext(context.Background(), req)\n}\n\nfunc (c *Client) WebsiteSearchWithContext(ctx context.Context, req *WebsiteSearchRequest) (*WebsiteSearchResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/websites/search\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &WebsiteSearchResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/v2/api_website_ssl_get.go",
    "content": "package v2\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype WebsiteSSLGetResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tID            int64  `json:\"id\"`\n\t\tProvider      string `json:\"provider\"`\n\t\tDescription   string `json:\"description\"`\n\t\tPrimaryDomain string `json:\"primaryDomain\"`\n\t\tDomains       string `json:\"domains\"`\n\t\tType          string `json:\"type\"`\n\t\tOrganization  string `json:\"organization\"`\n\t\tStatus        string `json:\"status\"`\n\t\tStartDate     string `json:\"startDate\"`\n\t\tExpireDate    string `json:\"expireDate\"`\n\t\tCreatedAt     string `json:\"createdAt\"`\n\t\tUpdatedAt     string `json:\"updatedAt\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) WebsiteSSLGet(sslId int64) (*WebsiteSSLGetResponse, error) {\n\treturn c.WebsiteSSLGetWithContext(context.Background(), sslId)\n}\n\nfunc (c *Client) WebsiteSSLGetWithContext(ctx context.Context, sslId int64) (*WebsiteSSLGetResponse, error) {\n\tif sslId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset sslId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf(\"/websites/ssl/%d\", sslId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &WebsiteSSLGetResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/v2/api_website_ssl_search.go",
    "content": "package v2\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype WebsiteSSLSearchRequest struct {\n\tDomain   string `json:\"domain\"`\n\tOrder    string `json:\"order\"`\n\tOrderBy  string `json:\"orderBy\"`\n\tPage     int32  `json:\"page\"`\n\tPageSize int32  `json:\"pageSize\"`\n}\n\ntype WebsiteSSLSearchResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tItems []*struct {\n\t\t\tID          int64  `json:\"id\"`\n\t\t\tPEM         string `json:\"pem\"`\n\t\t\tPrivateKey  string `json:\"privateKey\"`\n\t\t\tDomains     string `json:\"domains\"`\n\t\t\tDescription string `json:\"description\"`\n\t\t\tStatus      string `json:\"status\"`\n\t\t\tUpdatedAt   string `json:\"updatedAt\"`\n\t\t\tCreatedAt   string `json:\"createdAt\"`\n\t\t} `json:\"items\"`\n\t\tTotal int32 `json:\"total\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) WebsiteSSLSearch(req *WebsiteSSLSearchRequest) (*WebsiteSSLSearchResponse, error) {\n\treturn c.WebsiteSSLSearchWithContext(context.Background(), req)\n}\n\nfunc (c *Client) WebsiteSSLSearchWithContext(ctx context.Context, req *WebsiteSSLSearchRequest) (*WebsiteSSLSearchResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/websites/ssl/search\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &WebsiteSSLSearchResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/v2/api_website_ssl_upload.go",
    "content": "package v2\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype WebsiteSSLUploadRequest struct {\n\tSSLID           int64  `json:\"sslID\"`\n\tType            string `json:\"type\"`\n\tCertificate     string `json:\"certificate\"`\n\tCertificatePath string `json:\"certificatePath\"`\n\tPrivateKey      string `json:\"privateKey\"`\n\tPrivateKeyPath  string `json:\"privateKeyPath\"`\n\tDescription     string `json:\"description\"`\n}\n\ntype WebsiteSSLUploadResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) WebsiteSSLUpload(req *WebsiteSSLUploadRequest) (*WebsiteSSLUploadResponse, error) {\n\treturn c.WebsiteSSLUploadWithContext(context.Background(), req)\n}\n\nfunc (c *Client) WebsiteSSLUploadWithContext(ctx context.Context, req *WebsiteSSLUploadRequest) (*WebsiteSSLUploadResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/websites/ssl/upload\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &WebsiteSSLUploadResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/v2/client.go",
    "content": "package v2\n\nimport (\n\t\"crypto/md5\"\n\t\"crypto/tls\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl, apiKey string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif apiKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiKey\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")+\"/api/v2\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\ttimestamp := fmt.Sprintf(\"%d\", time.Now().Unix())\n\t\t\ttokenMd5 := md5.Sum([]byte(\"1panel\" + apiKey + timestamp))\n\t\t\ttokenMd5Hex := hex.EncodeToString(tokenMd5[:])\n\t\t\treq.Header.Set(\"1Panel-Timestamp\", timestamp)\n\t\t\treq.Header.Set(\"1Panel-Token\", tokenMd5Hex)\n\n\t\t\treturn nil\n\t\t})\n\n\treturn &Client{client}, nil\n}\n\nfunc NewClientWithNode(serverUrl, apiKey, node string) (*Client, error) {\n\tclient, err := NewClient(serverUrl, apiKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif node == \"\" {\n\t\tnode = \"local\"\n\t}\n\tclient.client.SetHeader(\"CurrentNode\", node)\n\n\treturn client, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tcode := res.GetCode(); tcode/100 != 2 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: code='%d', message='%s'\", tcode, res.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/1panel/v2/types.go",
    "content": "package v2\n\ntype sdkResponse interface {\n\tGetCode() int\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    *int    `json:\"code,omitempty\"`\n\tMessage *string `json:\"message,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() int {\n\tif r.Code == nil {\n\t\treturn 0\n\t}\n\n\treturn *r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n"
  },
  {
    "path": "pkg/sdk3rd/51dnscom/api_domain_list.go",
    "content": "package dnscom\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype DomainListRequest struct {\n\tGroupID  *string `json:\"groupID,omitempty\"`\n\tPage     *int32  `json:\"page,omitempty\"`\n\tPageSize *int32  `json:\"pageSize,omitempty\"`\n}\n\ntype DomainListResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tData      []*DomainRecord `json:\"data\"`\n\t\tPage      int32           `json:\"page\"`\n\t\tPageSize  int32           `json:\"pageSize\"`\n\t\tPageCount int32           `json:\"pageCount\"`\n\t} `json:\"data\"`\n}\n\nfunc (c *Client) DomainList(req *DomainListRequest) (*DomainListResponse, error) {\n\treturn c.DomainListWithContext(context.Background(), req)\n}\n\nfunc (c *Client) DomainListWithContext(ctx context.Context, req *DomainListRequest) (*DomainListResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/domain/list/\", req)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &DomainListResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/51dnscom/api_record_create.go",
    "content": "package dnscom\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype RecordCreateRequest struct {\n\tDomainID *string `json:\"domainID,omitempty\"`\n\tViewID   *string `json:\"viewID,omitempty\"`\n\tType     *string `json:\"type,omitempty\"`\n\tHost     *string `json:\"host,omitempty\"`\n\tValue    *string `json:\"value,omitempty\"`\n\tTTL      *int32  `json:\"ttl,omitempty\"`\n\tMX       *int32  `json:\"mx,omitempty\"`\n\tRemark   *string `json:\"remark,omitempty\"`\n}\n\ntype RecordCreateResponse struct {\n\tsdkResponseBase\n\n\tData *DNSRecord `json:\"data\"`\n}\n\nfunc (c *Client) RecordCreate(req *RecordCreateRequest) (*RecordCreateResponse, error) {\n\treturn c.RecordCreateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) RecordCreateWithContext(ctx context.Context, req *RecordCreateRequest) (*RecordCreateResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/record/create/\", req)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &RecordCreateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/51dnscom/api_record_remove.go",
    "content": "package dnscom\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype RecordRemoveRequest struct {\n\tDomainID *string `json:\"domainID,omitempty\"`\n\tRecordID *string `json:\"recordID,omitempty\"`\n}\n\ntype RecordRemoveResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) RecordRemove(req *RecordRemoveRequest) (*RecordRemoveResponse, error) {\n\treturn c.RecordRemoveWithContext(context.Background(), req)\n}\n\nfunc (c *Client) RecordRemoveWithContext(ctx context.Context, req *RecordRemoveRequest) (*RecordRemoveResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/record/remove/\", req)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &RecordRemoveResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/51dnscom/client.go",
    "content": "package dnscom\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tapiKey    string\n\tapiSecret string\n\n\tclient *resty.Client\n}\n\nfunc NewClient(apiKey, apiSecret string) (*Client, error) {\n\tif apiKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiKey\")\n\t}\n\tif apiSecret == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiSecret\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(\"https://www.51dns.com/api\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent)\n\n\treturn &Client{\n\t\tapiKey:    apiKey,\n\t\tapiSecret: apiSecret,\n\t\tclient:    client,\n\t}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string, params any) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\tdata := make(map[string]string)\n\tif params != nil {\n\t\ttemp := make(map[string]any)\n\t\tjsonb, _ := json.Marshal(params)\n\t\tjson.Unmarshal(jsonb, &temp)\n\t\tfor k, v := range temp {\n\t\t\tif v == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdata[k] = fmt.Sprintf(\"%v\", v)\n\t\t}\n\t}\n\n\tdata[\"apiKey\"] = c.apiKey\n\tdata[\"timestamp\"] = fmt.Sprintf(\"%d\", time.Now().Unix())\n\tdata[\"hash\"] = generateHash(data, c.apiSecret)\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treq.SetBody(data)\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetBody` or `req.SetFormData` HERE! USE `newRequest` INSTEAD.\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tcode := res.GetCode(); tcode != 0 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: code='%d', message='%s'\", tcode, res.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n\nfunc generateHash(params map[string]string, secert string) string {\n\tvar keyList []string\n\tfor k := range params {\n\t\tkeyList = append(keyList, k)\n\t}\n\tsort.Strings(keyList)\n\n\tvar hashString string\n\tfor _, key := range keyList {\n\t\tif hashString == \"\" {\n\t\t\thashString += key + \"=\" + params[key]\n\t\t} else {\n\t\t\thashString += \"&\" + key + \"=\" + params[key]\n\t\t}\n\t}\n\n\tm := md5.New()\n\tm.Write([]byte(hashString + secert))\n\tcipherStr := m.Sum(nil)\n\treturn hex.EncodeToString(cipherStr)\n}\n"
  },
  {
    "path": "pkg/sdk3rd/51dnscom/types.go",
    "content": "package dnscom\n\nimport (\n\t\"encoding/json\"\n)\n\ntype sdkResponse interface {\n\tGetCode() int\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() int {\n\treturn r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\treturn r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype DomainRecord struct {\n\tGroupID        json.Number `json:\"groupID\"`\n\tDomainID       json.Number `json:\"domainsID\"`\n\tDomain         string      `json:\"domains\"`\n\tState          int32       `json:\"state\"`\n\tUserLockState  int32       `json:\"userLock\"`\n\tAdminLockState int32       `json:\"adminLock\"`\n\tHealthState    int32       `json:\"healthState\"`\n\tViewType       string      `json:\"view_type\"`\n}\n\ntype DNSRecord struct {\n\tDomainID json.Number `json:\"domainID\"`\n\tRecordID json.Number `json:\"recordID\"`\n\tViewID   json.Number `json:\"viewID\"`\n\tRecord   string      `json:\"record\"`\n\tType     string      `json:\"type\"`\n\tHost     string      `json:\"host\"`\n\tValue    string      `json:\"value\"`\n\tTTL      int32       `json:\"ttl\"`\n\tMX       int32       `json:\"mx\"`\n\tState    int32       `json:\"state\"`\n\tRemark   string      `json:\"remark\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/apisix/api_ssl_update.go",
    "content": "package apisix\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype SslUpdateRequest = SslCertificate\n\ntype SslUpdateResponse = SslCertificate\n\nfunc (c *Client) SslUpdate(sslId string, req *SslUpdateRequest) (*SslUpdateResponse, error) {\n\treturn c.SslUpdateWithContext(context.Background(), sslId, req)\n}\n\nfunc (c *Client) SslUpdateWithContext(ctx context.Context, sslId string, req *SslUpdateRequest) (*SslUpdateResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf(\"/ssls/%s\", url.PathEscape(sslId)))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SslUpdateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/apisix/client.go",
    "content": "package apisix\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl, apiKey string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif apiKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiKey\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")+\"/apisix/admin\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetHeader(\"X-Api-Key\", apiKey)\n\n\treturn &Client{\n\t\tclient: client,\n\t}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res interface{}) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/apisix/types.go",
    "content": "package apisix\n\ntype SslCertificate struct {\n\tID            *string            `json:\"id,omitempty\"`\n\tStatus        *int32             `json:\"status,omitempty\"`\n\tCertificate   *string            `json:\"cert,omitempty\"`\n\tPrivateKey    *string            `json:\"key,omitempty\"`\n\tSNIs          *[]string          `json:\"snis,omitempty\"`\n\tType          *string            `json:\"type,omitempty\"`\n\tValidityStart *int64             `json:\"validity_start,omitempty\"`\n\tValidityEnd   *int64             `json:\"validity_end,omitempty\"`\n\tLabels        *map[string]string `json:\"labels,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/azure/env/config.go",
    "content": "package env\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud\"\n)\n\nfunc IsPublicEnv(env string) bool {\n\tswitch strings.ToLower(env) {\n\tcase \"\", \"default\", \"public\", \"azurecloud\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc IsUSGovernmentEnv(env string) bool {\n\tswitch strings.ToLower(env) {\n\tcase \"usgovernment\", \"government\", \"azureusgovernment\", \"azuregovernment\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc IsChinaEnv(env string) bool {\n\tswitch strings.ToLower(env) {\n\tcase \"china\", \"chinacloud\", \"azurechina\", \"azurechinacloud\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc GetCloudEnvConfiguration(env string) (cloud.Configuration, error) {\n\tif IsPublicEnv(env) {\n\t\treturn cloud.AzurePublic, nil\n\t} else if IsUSGovernmentEnv(env) {\n\t\treturn cloud.AzureGovernment, nil\n\t} else if IsChinaEnv(env) {\n\t\treturn cloud.AzureChina, nil\n\t}\n\n\treturn cloud.Configuration{}, fmt.Errorf(\"unknown azure cloud environment %s\", env)\n}\n"
  },
  {
    "path": "pkg/sdk3rd/baiducloud/cert/cert.go",
    "content": "package cert\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/baidubce/bce-sdk-go/bce\"\n\t\"github.com/baidubce/bce-sdk-go/http\"\n\t\"github.com/baidubce/bce-sdk-go/services/cert\"\n)\n\nfunc (c *Client) CreateCert(args *CreateCertArgs) (*CreateCertResult, error) {\n\tif args == nil {\n\t\treturn nil, errors.New(\"unset args\")\n\t}\n\n\tresult, err := c.Client.CreateCert(&args.CreateCertArgs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &CreateCertResult{CreateCertResult: *result}, nil\n}\n\nfunc (c *Client) ListCerts() (*ListCertResult, error) {\n\tresult, err := c.Client.ListCerts()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &ListCertResult{ListCertResult: *result}, nil\n}\n\nfunc (c *Client) ListCertDetail() (*ListCertDetailResult, error) {\n\tresult, err := c.Client.ListCertDetail()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &ListCertDetailResult{ListCertDetailResult: *result}, nil\n}\n\nfunc (c *Client) GetCertMeta(id string) (*CertificateMeta, error) {\n\tresult, err := c.Client.GetCertMeta(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &CertificateMeta{CertificateMeta: *result}, nil\n}\n\nfunc (c *Client) GetCertDetail(id string) (*CertificateDetailMeta, error) {\n\tresult, err := c.Client.GetCertDetail(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &CertificateDetailMeta{CertificateDetailMeta: *result}, nil\n}\n\nfunc (c *Client) GetCertRawData(id string) (*CertificateRawData, error) {\n\tresult := &CertificateRawData{}\n\terr := bce.NewRequestBuilder(c).\n\t\tWithMethod(http.GET).\n\t\tWithURL(cert.URI_PREFIX + cert.REQUEST_CERT_URL + \"/\" + id + \"/rawData\").\n\t\tWithResult(result).\n\t\tDo()\n\n\treturn result, err\n}\n\nfunc (c *Client) UpdateCertName(id string, args *UpdateCertNameArgs) error {\n\tif args == nil {\n\t\treturn errors.New(\"unset args\")\n\t}\n\n\terr := c.Client.UpdateCertName(id, &args.UpdateCertNameArgs)\n\treturn err\n}\n\nfunc (c *Client) UpdateCertData(id string, args *UpdateCertDataArgs) error {\n\tif args == nil {\n\t\treturn fmt.Errorf(\"unset args\")\n\t}\n\n\terr := c.Client.UpdateCertData(id, &args.UpdateCertDataArgs)\n\treturn err\n}\n\nfunc (c *Client) DeleteCert(id string) error {\n\terr := c.Client.DeleteCert(id)\n\treturn err\n}\n"
  },
  {
    "path": "pkg/sdk3rd/baiducloud/cert/client.go",
    "content": "package cert\n\nimport (\n\t\"github.com/baidubce/bce-sdk-go/services/cert\"\n)\n\ntype Client struct {\n\t*cert.Client\n}\n\nfunc NewClient(ak, sk, endPoint string) (*Client, error) {\n\tclient, err := cert.NewClient(ak, sk, endPoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{client}, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/baiducloud/cert/model.go",
    "content": "package cert\n\nimport \"github.com/baidubce/bce-sdk-go/services/cert\"\n\ntype CreateCertArgs struct {\n\tcert.CreateCertArgs\n}\n\ntype CreateCertResult struct {\n\tcert.CreateCertResult\n}\n\ntype UpdateCertNameArgs struct {\n\tcert.UpdateCertNameArgs\n}\n\ntype CertificateMeta struct {\n\tcert.CertificateMeta\n}\n\ntype CertificateDetailMeta struct {\n\tcert.CertificateDetailMeta\n}\n\ntype CertificateRawData struct {\n\tCertId          string `json:\"certId\"`\n\tCertName        string `json:\"certName\"`\n\tCertServerData  string `json:\"certServerData\"`\n\tCertPrivateData string `json:\"certPrivateKey\"`\n\tCertLinkData    string `json:\"certLinkData,omitempty\"`\n\tCertType        int    `json:\"certType,omitempty\"`\n}\n\ntype ListCertResult struct {\n\tcert.ListCertResult\n}\n\ntype ListCertDetailResult struct {\n\tcert.ListCertDetailResult\n}\n\ntype UpdateCertDataArgs struct {\n\tcert.UpdateCertDataArgs\n}\n\ntype CertInServiceMeta struct {\n\tcert.CertInServiceMeta\n}\n"
  },
  {
    "path": "pkg/sdk3rd/baishan/api_get_domain_config.go",
    "content": "package baishan\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype GetDomainConfigRequest struct {\n\tDomains *string   `json:\"domains,omitempty\" url:\"domains,omitempty\"`\n\tConfig  *[]string `json:\"config,omitempty\"  url:\"config,omitempty\"`\n}\n\ntype GetDomainConfigResponse struct {\n\tsdkResponseBase\n\n\tData []*struct {\n\t\tDomain string        `json:\"domain\"`\n\t\tConfig *DomainConfig `json:\"config\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) GetDomainConfig(req *GetDomainConfigRequest) (*GetDomainConfigResponse, error) {\n\treturn c.GetDomainConfigWithContext(context.Background(), req)\n}\n\nfunc (c *Client) GetDomainConfigWithContext(ctx context.Context, req *GetDomainConfigRequest) (*GetDomainConfigResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/v2/domain/config\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tif req.Domains != nil {\n\t\t\thttpreq.SetQueryParam(\"domains\", *req.Domains)\n\t\t}\n\t\tif req.Config != nil {\n\t\t\tfor _, config := range *req.Config {\n\t\t\t\thttpreq.QueryParam.Add(\"config[]\", config)\n\t\t\t}\n\t\t}\n\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &GetDomainConfigResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/baishan/api_get_domain_list.go",
    "content": "package baishan\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype GetDomainListRequest struct {\n\tPageNumber   *int32  `json:\"page_number,omitempty\"   url:\"page_number,omitempty\"`\n\tPageSize     *int32  `json:\"page_size,omitempty\"     url:\"page_size,omitempty\"`\n\tDomainStatus *string `json:\"domain_status,omitempty\" url:\"domain_status,omitempty\"`\n}\n\ntype GetDomainListResponse struct {\n\tsdkResponseBase\n\n\tData []*struct {\n\t\tList        []*DomainRecord `json:\"list\"`\n\t\tPageNumber  json.Number     `json:\"page_number\"`\n\t\tPageSize    json.Number     `json:\"page_size\"`\n\t\tTotalNumber json.Number     `json:\"total_number\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) GetDomainList(req *GetDomainListRequest) (*GetDomainListResponse, error) {\n\treturn c.GetDomainListWithContext(context.Background(), req)\n}\n\nfunc (c *Client) GetDomainListWithContext(ctx context.Context, req *GetDomainListRequest) (*GetDomainListResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/v2/domain/list\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &GetDomainListResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/baishan/api_set_domain_config.go",
    "content": "package baishan\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype SetDomainConfigRequest struct {\n\tDomains *string       `json:\"domains,omitempty\"`\n\tConfig  *DomainConfig `json:\"config,omitempty\"`\n}\n\ntype SetDomainConfigResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tConfig *DomainConfig `json:\"config\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) SetDomainConfig(req *SetDomainConfigRequest) (*SetDomainConfigResponse, error) {\n\treturn c.SetDomainConfigWithContext(context.Background(), req)\n}\n\nfunc (c *Client) SetDomainConfigWithContext(ctx context.Context, req *SetDomainConfigRequest) (*SetDomainConfigResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/v2/domain/config\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SetDomainConfigResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/baishan/api_upload_domain_certificate.go",
    "content": "package baishan\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype UploadDomainCertificateRequest struct {\n\tCertificateId *string `json:\"cert_id,omitempty\"`\n\tCertificate   *string `json:\"certificate,omitempty\"`\n\tKey           *string `json:\"key,omitempty\"`\n\tName          *string `json:\"name,omitempty\"`\n}\n\ntype UploadDomainCertificateResponse struct {\n\tsdkResponseBase\n\n\tData *DomainCertificate `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) UploadDomainCertificate(req *UploadDomainCertificateRequest) (*UploadDomainCertificateResponse, error) {\n\treturn c.UploadDomainCertificateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) UploadDomainCertificateWithContext(ctx context.Context, req *UploadDomainCertificateRequest) (*UploadDomainCertificateResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/v2/domain/certificate\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UploadDomainCertificateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/baishan/client.go",
    "content": "package baishan\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(apiToken string) (*Client, error) {\n\tif apiToken == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiToken\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(\"https://cdn.api.baishan.com\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetQueryParam(\"token\", apiToken)\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tcode := res.GetCode(); tcode != 0 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: code='%d', message='%s'\", tcode, res.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/baishan/types.go",
    "content": "package baishan\n\nimport (\n\t\"encoding/json\"\n)\n\ntype sdkResponse interface {\n\tGetCode() int\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    *int    `json:\"code,omitempty\"`\n\tMessage *string `json:\"message,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() int {\n\tif r.Code == nil {\n\t\treturn 0\n\t}\n\n\treturn *r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype DomainRecord struct {\n\tId         string `json:\"id\"`\n\tDomain     string `json:\"domain\"`\n\tType       string `json:\"type\"`\n\tStatus     string `json:\"status\"`\n\tCname      string `json:\"cname\"`\n\tArea       string `json:\"area\"`\n\tCreateTime string `json:\"create_time\"`\n\tUpdateTime string `json:\"update_time\"`\n}\n\ntype DomainCertificate struct {\n\tCertId         json.Number `json:\"cert_id\"`\n\tName           string      `json:\"name\"`\n\tCertStartTime  string      `json:\"cert_start_time\"`\n\tCertExpireTime string      `json:\"cert_expire_time\"`\n}\n\ntype DomainConfig struct {\n\tHttps *DomainConfigHttps `json:\"https\"`\n}\n\ntype DomainConfigHttps struct {\n\tCertId      json.Number `json:\"cert_id\"`\n\tForceHttps  *string     `json:\"force_https,omitempty\"`\n\tEnableHttp2 *string     `json:\"http2,omitempty\"`\n\tEnableOcsp  *string     `json:\"ocsp,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanel/api_config_save_panel_ssl.go",
    "content": "package btpanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype ConfigSavePanelSSLRequest struct {\n\tPrivateKey  string `json:\"privateKey\"`\n\tCertificate string `json:\"certPem\"`\n}\n\ntype ConfigSavePanelSSLResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) ConfigSavePanelSSL(req *ConfigSavePanelSSLRequest) (*ConfigSavePanelSSLResponse, error) {\n\treturn c.ConfigSavePanelSSLWithContext(context.Background(), req)\n}\n\nfunc (c *Client) ConfigSavePanelSSLWithContext(ctx context.Context, req *ConfigSavePanelSSLRequest) (*ConfigSavePanelSSLResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/config?action=SavePanelSSL\", req)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ConfigSavePanelSSLResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanel/api_mod_proxy_com_set_ssl.go",
    "content": "package btpanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype ModProxyComSetSSLRequest struct {\n\tSiteName    string `json:\"site_name\"`\n\tPrivateKey  string `json:\"key\"`\n\tCertificate string `json:\"csr\"`\n}\n\ntype ModProxyComSetSSLResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) ModProxyComSetSSL(req *ModProxyComSetSSLRequest) (*ModProxyComSetSSLResponse, error) {\n\treturn c.ModProxyComSetSSLWithContext(context.Background(), req)\n}\n\nfunc (c *Client) ModProxyComSetSSLWithContext(ctx context.Context, req *ModProxyComSetSSLRequest) (*ModProxyComSetSSLResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/mod/proxy/com/set_ssl\", req)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ModProxyComSetSSLResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanel/api_site_set_ssl.go",
    "content": "package btpanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype SiteSetSSLRequest struct {\n\tType        string `json:\"type\"`\n\tSiteName    string `json:\"siteName\"`\n\tPrivateKey  string `json:\"key\"`\n\tCertificate string `json:\"csr\"`\n}\n\ntype SiteSetSSLResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) SiteSetSSL(req *SiteSetSSLRequest) (*SiteSetSSLResponse, error) {\n\treturn c.SiteSetSSLWithContext(context.Background(), req)\n}\n\nfunc (c *Client) SiteSetSSLWithContext(ctx context.Context, req *SiteSetSSLRequest) (*SiteSetSSLResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/site?action=SetSSL\", req)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SiteSetSSLResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanel/api_ssl_cert_save_cert.go",
    "content": "package btpanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype SSLCertSaveCertRequest struct {\n\tPrivateKey  string `json:\"key\"`\n\tCertificate string `json:\"csr\"`\n}\n\ntype SSLCertSaveCertResponse struct {\n\tsdkResponseBase\n\n\tSSLHash string `json:\"ssl_hash\"`\n}\n\nfunc (c *Client) SSLCertSaveCert(req *SSLCertSaveCertRequest) (*SSLCertSaveCertResponse, error) {\n\treturn c.SSLCertSaveCertWithContext(context.Background(), req)\n}\n\nfunc (c *Client) SSLCertSaveCertWithContext(ctx context.Context, req *SSLCertSaveCertRequest) (*SSLCertSaveCertResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/ssl/cert/save_cert\", req)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SSLCertSaveCertResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanel/api_ssl_set_batch_cert_to_site.go",
    "content": "package btpanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype SSLSetBatchCertToSiteRequest struct {\n\tBatchInfo []*SSLSetBatchCertToSiteRequestBatchInfo `json:\"BatchInfo\"`\n}\n\ntype SSLSetBatchCertToSiteRequestBatchInfo struct {\n\tSSLHash  string `json:\"ssl_hash\"`\n\tSiteName string `json:\"siteName\"`\n\tCertName string `json:\"certName\"`\n}\n\ntype SSLSetBatchCertToSiteResponse struct {\n\tsdkResponseBase\n\n\tTotalCount   int32 `json:\"total\"`\n\tSuccessCount int32 `json:\"success\"`\n\tFailedCount  int32 `json:\"faild\"`\n}\n\nfunc (c *Client) SSLSetBatchCertToSite(req *SSLSetBatchCertToSiteRequest) (*SSLSetBatchCertToSiteResponse, error) {\n\treturn c.SSLSetBatchCertToSiteWithContext(context.Background(), req)\n}\n\nfunc (c *Client) SSLSetBatchCertToSiteWithContext(ctx context.Context, req *SSLSetBatchCertToSiteRequest) (*SSLSetBatchCertToSiteResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/ssl?action=SetBatchCertToSite\", req)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SSLSetBatchCertToSiteResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanel/api_system_service_admin.go",
    "content": "package btpanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype SystemServiceAdminRequest struct {\n\tName string `json:\"name\"`\n\tType string `json:\"type\"`\n}\n\ntype SystemServiceAdminResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) SystemServiceAdmin(req *SystemServiceAdminRequest) (*SystemServiceAdminResponse, error) {\n\treturn c.SystemServiceAdminWithContext(context.Background(), req)\n}\n\nfunc (c *Client) SystemServiceAdminWithContext(ctx context.Context, req *SystemServiceAdminRequest) (*SystemServiceAdminResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/system?action=ServiceAdmin\", req)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SystemServiceAdminResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanel/client.go",
    "content": "package btpanel\n\nimport (\n\t\"crypto/md5\"\n\t\"crypto/tls\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tapiKey string\n\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl, apiKey string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif apiKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiKey\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")).\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/x-www-form-urlencoded\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent)\n\n\treturn &Client{\n\t\tapiKey: apiKey,\n\t\tclient: client,\n\t}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string, params any) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\tdata := make(map[string]string)\n\tif params != nil {\n\t\ttemp := make(map[string]any)\n\t\tjsonb, _ := json.Marshal(params)\n\t\tjson.Unmarshal(jsonb, &temp)\n\t\tfor k, v := range temp {\n\t\t\tif v == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tswitch reflect.Indirect(reflect.ValueOf(v)).Kind() {\n\t\t\tcase reflect.String:\n\t\t\t\tdata[k] = v.(string)\n\n\t\t\tcase reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:\n\t\t\t\tdata[k] = fmt.Sprintf(\"%v\", v)\n\n\t\t\tdefault:\n\t\t\t\tif t, ok := v.(time.Time); ok {\n\t\t\t\t\tdata[k] = t.Format(time.RFC3339)\n\t\t\t\t} else {\n\t\t\t\t\tjsonb, _ := json.Marshal(v)\n\t\t\t\t\tdata[k] = string(jsonb)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\ttimestamp := time.Now().Unix()\n\tdata[\"request_time\"] = fmt.Sprintf(\"%d\", timestamp)\n\tdata[\"request_token\"] = generateSignature(fmt.Sprintf(\"%d\", timestamp), c.apiKey)\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treq.SetFormData(data)\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetBody` or `req.SetFormData` HERE! USE `newRequest` INSTEAD.\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tstatus := res.GetStatus(); tstatus != nil && !*tstatus {\n\t\t\t\tif res.GetMessage() == nil {\n\t\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: unknown error\")\n\t\t\t\t} else {\n\t\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: message='%s'\", *res.GetMessage())\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n\nfunc generateSignature(timestamp string, apiKey string) string {\n\tkeyMd5 := md5.Sum([]byte(apiKey))\n\tkeyMd5Hex := strings.ToLower(hex.EncodeToString(keyMd5[:]))\n\n\tsignMd5 := md5.Sum([]byte(timestamp + keyMd5Hex))\n\tsignMd5Hex := strings.ToLower(hex.EncodeToString(signMd5[:]))\n\treturn signMd5Hex\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanel/types.go",
    "content": "package btpanel\n\ntype sdkResponse interface {\n\tGetStatus() *bool\n\tGetMessage() *string\n}\n\ntype sdkResponseBase struct {\n\tStatus  *bool   `json:\"status,omitempty\"`\n\tMessage *string `json:\"msg,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetStatus() *bool {\n\treturn r.Status\n}\n\nfunc (r *sdkResponseBase) GetMessage() *string {\n\treturn r.Message\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanelgo/api_config_set_panel_ssl.go",
    "content": "package btpanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype ConfigSetPanelSSLRequest struct {\n\tSSLStatus *int32  `json:\"ssl_status,omitempty\"`\n\tSSLKey    *string `json:\"ssl_key,omitempty\"`\n\tSSLPem    *string `json:\"ssl_pem,omitempty\"`\n}\n\ntype ConfigSetPanelSSLResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) ConfigSetPanelSSL(req *ConfigSetPanelSSLRequest) (*ConfigSetPanelSSLResponse, error) {\n\treturn c.ConfigSetPanelSSLWithContext(context.Background(), req)\n}\n\nfunc (c *Client) ConfigSetPanelSSLWithContext(ctx context.Context, req *ConfigSetPanelSSLRequest) (*ConfigSetPanelSSLResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/config/set_panel_ssl\", req, false)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ConfigSetPanelSSLResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanelgo/api_datalist_get_data_list.go",
    "content": "package btpanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype DatalistGetDataListRequest struct {\n\tTable        *string `json:\"table,omitempty\"`\n\tSearchType   *string `json:\"search_type,omitempty\"`\n\tSearchString *string `json:\"search,omitempty\"`\n\tPage         *int32  `json:\"p,omitempty\"`\n\tLimit        *int32  `json:\"limit,omitempty\"`\n\tOrder        *string `json:\"order,omitempty\"`\n\tType         *int32  `json:\"type,omitempty\"`\n}\n\ntype DatalistGetDataListResponse struct {\n\tsdkResponseBase\n\tData []*SiteData `json:\"data,omitempty\"`\n\tPage *PageData   `json:\"page,omitempty\"`\n}\n\nfunc (c *Client) DatalistGetDataList(req *DatalistGetDataListRequest) (*DatalistGetDataListResponse, error) {\n\treturn c.DatalistGetDataListWithContext(context.Background(), req)\n}\n\nfunc (c *Client) DatalistGetDataListWithContext(ctx context.Context, req *DatalistGetDataListRequest) (*DatalistGetDataListResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/datalist/get_data_list\", req, false)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &DatalistGetDataListResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanelgo/api_files_upload.go",
    "content": "package btpanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype FilesUploadRequest struct {\n\tPath  *string `json:\"path,omitempty\"`\n\tName  *string `json:\"filename,omitempty\"`\n\tStart *int32  `json:\"start,omitempty\"`\n\tSize  *int32  `json:\"size,omitempty\"`\n\tBlob  []byte  `json:\"-\" form:\"blob\"`\n\tForce *bool   `json:\"force,omitempty\"`\n}\n\ntype FilesUploadResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) FilesUpload(req *FilesUploadRequest) (*FilesUploadResponse, error) {\n\treturn c.FilesUploadWithContext(context.Background(), req)\n}\n\nfunc (c *Client) FilesUploadWithContext(ctx context.Context, req *FilesUploadRequest) (*FilesUploadResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/files/upload\", req, true)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &FilesUploadResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanelgo/api_panel_get_config.go",
    "content": "package btpanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype PanelGetConfigRequest struct{}\n\ntype PanelGetConfigResponse struct {\n\tsdkResponseBase\n\n\tPaths *struct {\n\t\tPanel string `json:\"panel,omitempty\"`\n\t\tSoft  string `json:\"soft,omitempty\"`\n\t} `json:\"paths,omitempty\"`\n\tSite *struct {\n\t\tWebServer  string `json:\"webserver,omitempty\"`\n\t\tSitesPath  string `json:\"sites_path,omitempty\"`\n\t\tBackupPath string `json:\"backup_path,omitempty\"`\n\t} `json:\"site,omitempty\"`\n}\n\nfunc (c *Client) PanelGetConfig(req *PanelGetConfigRequest) (*PanelGetConfigResponse, error) {\n\treturn c.PanelGetConfigWithContext(context.Background(), req)\n}\n\nfunc (c *Client) PanelGetConfigWithContext(ctx context.Context, req *PanelGetConfigRequest) (*PanelGetConfigResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/panel/get_config\", req, false)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &PanelGetConfigResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanelgo/api_site_get_project_list.go",
    "content": "package btpanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype SiteGetProjectListRequest struct {\n\tSearchType   *string `json:\"search_type,omitempty\"`\n\tSearchString *string `json:\"search,omitempty\"`\n\tPage         *int32  `json:\"p,omitempty\"`\n\tLimit        *int32  `json:\"limit,omitempty\"`\n\tOrder        *string `json:\"order,omitempty\"`\n}\n\ntype SiteGetProjectListResponse struct {\n\tsdkResponseBase\n\tData []*SiteData `json:\"data,omitempty\"`\n\tPage *PageData   `json:\"page,omitempty\"`\n}\n\nfunc (c *Client) SiteGetProjectList(req *SiteGetProjectListRequest) (*SiteGetProjectListResponse, error) {\n\treturn c.SiteGetProjectListWithContext(context.Background(), req)\n}\n\nfunc (c *Client) SiteGetProjectListWithContext(ctx context.Context, req *SiteGetProjectListRequest) (*SiteGetProjectListResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/site/get_project_list\", req, false)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SiteGetProjectListResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanelgo/api_site_set_site_pfx_ssl.go",
    "content": "package btpanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype SiteSetSitePFXSSLRequest struct {\n\tSiteId   *int32  `json:\"siteid,omitempty\"`\n\tPFX      *string `json:\"pfx,omitempty\"`\n\tPassword *string `json:\"password,omitempty\"`\n}\n\ntype SiteSetSitePFXSSLResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) SiteSetSitePFXSSL(req *SiteSetSitePFXSSLRequest) (*SiteSetSitePFXSSLResponse, error) {\n\treturn c.SiteSetSitePFXSSLWithContext(context.Background(), req)\n}\n\nfunc (c *Client) SiteSetSitePFXSSLWithContext(ctx context.Context, req *SiteSetSitePFXSSLRequest) (*SiteSetSitePFXSSLResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/site/set_site_pfx_ssl\", req, false)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SiteSetSitePFXSSLResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanelgo/api_site_set_site_ssl.go",
    "content": "package btpanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype SiteSetSiteSSLRequest struct {\n\tSiteId *int32  `json:\"siteid,omitempty\"`\n\tStatus *bool   `json:\"status,omitempty\"`\n\tKey    *string `json:\"key,omitempty\"`\n\tCert   *string `json:\"cert,omitempty\"`\n}\n\ntype SiteSetSiteSSLResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) SiteSetSiteSSL(req *SiteSetSiteSSLRequest) (*SiteSetSiteSSLResponse, error) {\n\treturn c.SiteSetSiteSSLWithContext(context.Background(), req)\n}\n\nfunc (c *Client) SiteSetSiteSSLWithContext(ctx context.Context, req *SiteSetSiteSSLRequest) (*SiteSetSiteSSLResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/site/set_site_ssl\", req, false)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SiteSetSiteSSLResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanelgo/client.go",
    "content": "package btpanel\n\nimport (\n\t\"bytes\"\n\t\"crypto/md5\"\n\t\"crypto/tls\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tapiKey string\n\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl, apiKey string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif apiKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiKey\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")).\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/x-www-form-urlencoded\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent)\n\n\treturn &Client{\n\t\tapiKey: apiKey,\n\t\tclient: client,\n\t}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string, params any, multipart bool) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\tdata := make(map[string]string)\n\tif params != nil {\n\t\ttemp := make(map[string]any)\n\t\tjsonb, _ := json.Marshal(params)\n\t\tjson.Unmarshal(jsonb, &temp)\n\t\tfor k, v := range temp {\n\t\t\tif v == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tswitch reflect.Indirect(reflect.ValueOf(v)).Kind() {\n\t\t\tcase reflect.String:\n\t\t\t\tdata[k] = v.(string)\n\n\t\t\tcase reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:\n\t\t\t\tdata[k] = fmt.Sprintf(\"%v\", v)\n\n\t\t\tdefault:\n\t\t\t\tif t, ok := v.(time.Time); ok {\n\t\t\t\t\tdata[k] = t.Format(time.RFC3339)\n\t\t\t\t} else {\n\t\t\t\t\tjsonb, _ := json.Marshal(v)\n\t\t\t\t\tdata[k] = string(jsonb)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\ttimestamp := time.Now().Unix()\n\tdata[\"request_time\"] = fmt.Sprintf(\"%d\", timestamp)\n\tdata[\"request_token\"] = generateSignature(fmt.Sprintf(\"%d\", timestamp), c.apiKey)\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\tif multipart {\n\t\treq.SetMultipartFormData(data)\n\n\t\tif params != nil {\n\t\t\tvparams := reflect.ValueOf(params)\n\t\t\tif vparams.Kind() == reflect.Ptr {\n\t\t\t\tvparams = vparams.Elem()\n\t\t\t}\n\t\t\tif vparams.Kind() == reflect.Struct {\n\t\t\t\tvparamsTyp := vparams.Type()\n\t\t\t\tfor i := 0; i < vparams.NumField(); i++ {\n\t\t\t\t\tfield := vparamsTyp.Field(i)\n\t\t\t\t\tfieldVal := vparams.Field(i)\n\t\t\t\t\tif fieldVal.Kind() == reflect.Ptr && fieldVal.IsNil() {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tformTag := field.Tag.Get(\"form\")\n\t\t\t\t\tif formTag == \"\" {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tswitch v := fieldVal.Interface().(type) {\n\t\t\t\t\tcase []byte:\n\t\t\t\t\t\treq.SetMultipartField(formTag, formTag, \"\", bytes.NewReader(v))\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tpanic(\"unreachable\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tpanic(\"unreachable\")\n\t\t\t}\n\t\t}\n\t} else {\n\t\treq.SetFormData(data)\n\t}\n\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetBody` or `req.SetFormData` HERE! USE `newRequest` INSTEAD.\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tstatus := res.GetStatus(); tstatus != nil {\n\t\t\t\tvar errored bool\n\n\t\t\t\tvar bstatus bool\n\t\t\t\tif err := json.Unmarshal(tstatus, &bstatus); err == nil {\n\t\t\t\t\terrored = !bstatus\n\t\t\t\t}\n\n\t\t\t\tvar istatus int\n\t\t\t\tif err := json.Unmarshal(tstatus, &istatus); err == nil {\n\t\t\t\t\terrored = istatus != 0\n\t\t\t\t}\n\n\t\t\t\tif errored {\n\t\t\t\t\tif res.GetMessage() == nil {\n\t\t\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: unknown error\")\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: message='%s'\", *res.GetMessage())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n\nfunc generateSignature(timestamp string, apiKey string) string {\n\tkeyMd5 := md5.Sum([]byte(apiKey))\n\tkeyMd5Hex := strings.ToLower(hex.EncodeToString(keyMd5[:]))\n\n\tsignMd5 := md5.Sum([]byte(timestamp + keyMd5Hex))\n\tsignMd5Hex := strings.ToLower(hex.EncodeToString(signMd5[:]))\n\treturn signMd5Hex\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btpanelgo/types.go",
    "content": "package btpanel\n\nimport (\n\t\"encoding/json\"\n)\n\ntype sdkResponse interface {\n\tGetStatus() json.RawMessage\n\tGetMessage() *string\n}\n\ntype sdkResponseBase struct {\n\tStatus  json.RawMessage `json:\"status,omitempty\"`\n\tCode    *int            `json:\"code,omitempty\"`\n\tMessage *string         `json:\"msg,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetStatus() json.RawMessage {\n\treturn r.Status\n}\n\nfunc (r *sdkResponseBase) GetMessage() *string {\n\treturn r.Message\n}\n\ntype SiteData struct {\n\tId          int32  `json:\"id\"`\n\tProjectType string `json:\"project_type\"`\n\tName        string `json:\"name\"`\n\tNote        string `json:\"ps\"`\n\tStatus      string `json:\"status\"`\n\tSSLInfo     []*struct {\n\t\tName   string `json:\"name\"`\n\t\tStatus bool   `json:\"status\"`\n\t} `json:\"ssl_info\"`\n\tAddTime string `json:\"addtime\"`\n}\n\ntype PageData struct {\n\tPage    int32 `json:\"page\"`\n\tLimit   int32 `json:\"limit\"`\n\tTotal   int32 `json:\"total\"`\n\tStart   int32 `json:\"start\"`\n\tEnd     int32 `json:\"end\"`\n\tMaxPage int32 `json:\"maxPage\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btwaf/api_config_set_cert.go",
    "content": "package btwaf\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype ConfigSetCertRequest struct {\n\tCertContent *string `json:\"certContent,omitempty\"`\n\tKeyContent  *string `json:\"keyContent,omitempty\"`\n}\n\ntype ConfigSetCertResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) ConfigSetCert(req *ConfigSetCertRequest) (*ConfigSetCertResponse, error) {\n\treturn c.ConfigSetCertWithContext(context.Background(), req)\n}\n\nfunc (c *Client) ConfigSetCertWithContext(ctx context.Context, req *ConfigSetCertRequest) (*ConfigSetCertResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/config/set_cert\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ConfigSetCertResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btwaf/api_get_site_list.go",
    "content": "package btwaf\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype GetSiteListRequest struct {\n\tSiteName *string `json:\"site_name,omitempty\"`\n\tPage     *int32  `json:\"p,omitempty\"`\n\tPageSize *int32  `json:\"p_size,omitempty\"`\n}\n\ntype GetSiteListResponse struct {\n\tsdkResponseBase\n\n\tResult *struct {\n\t\tList  []*SiteRecord `json:\"list\"`\n\t\tTotal int32         `json:\"total\"`\n\t} `json:\"res,omitempty\"`\n}\n\nfunc (c *Client) GetSiteList(req *GetSiteListRequest) (*GetSiteListResponse, error) {\n\treturn c.GetSiteListWithContext(context.Background(), req)\n}\n\nfunc (c *Client) GetSiteListWithContext(ctx context.Context, req *GetSiteListRequest) (*GetSiteListResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/wafmastersite/get_site_list\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &GetSiteListResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btwaf/api_modify_site.go",
    "content": "package btwaf\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype ModifySiteRequest struct {\n\tSiteId *string            `json:\"site_id,omitempty\"`\n\tType   *string            `json:\"types,omitempty\"`\n\tServer *SiteServerInfoMod `json:\"server,omitempty\"`\n}\n\ntype ModifySiteResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) ModifySite(req *ModifySiteRequest) (*ModifySiteResponse, error) {\n\treturn c.ModifySiteWithContext(context.Background(), req)\n}\n\nfunc (c *Client) ModifySiteWithContext(ctx context.Context, req *ModifySiteRequest) (*ModifySiteResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/wafmastersite/modify_site\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ModifySiteResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btwaf/client.go",
    "content": "package btwaf\n\nimport (\n\t\"crypto/md5\"\n\t\"crypto/tls\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl, apiKey string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif apiKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiKey\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")+\"/api\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\ttimestamp := fmt.Sprintf(\"%d\", time.Now().Unix())\n\t\t\tkeyMd5 := md5.Sum([]byte(apiKey))\n\t\t\tkeyMd5Hex := strings.ToLower(hex.EncodeToString(keyMd5[:]))\n\t\t\tsignMd5 := md5.Sum([]byte(timestamp + keyMd5Hex))\n\t\t\tsignMd5Hex := strings.ToLower(hex.EncodeToString(signMd5[:]))\n\t\t\treq.Header.Set(\"waf_request_time\", timestamp)\n\t\t\treq.Header.Set(\"waf_request_token\", signMd5Hex)\n\n\t\t\treturn nil\n\t\t})\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif code := res.GetCode(); code != 0 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: code='%d'\", code)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/btwaf/types.go",
    "content": "package btwaf\n\ntype sdkResponse interface {\n\tGetCode() int\n}\n\ntype sdkResponseBase struct {\n\tCode *int `json:\"code,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() int {\n\tif r.Code == nil {\n\t\treturn 0\n\t}\n\n\treturn *r.Code\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype SiteRecord struct {\n\tSiteId      string   `json:\"site_id\"`\n\tSiteName    string   `json:\"site_name\"`\n\tType        string   `json:\"types\"`\n\tStatus      int32    `json:\"status\"`\n\tServerNames []string `json:\"server_name\"`\n\tCreateTime  int64    `json:\"create_time\"`\n\tUpdateTime  int64    `json:\"update_time\"`\n}\n\n// type SiteServerInfo struct {\n// \tListenSSLPorts *[]int32           `json:\"listen_ssl_port,omitempty\"`\n// \tSSL            *SiteServerSSLInfo `json:\"ssl,omitempty\"`\n// }\n\ntype SiteServerInfoMod struct {\n\tListenSSLPorts *[]string          `json:\"listen_ssl_port,omitempty\"`\n\tSSL            *SiteServerSSLInfo `json:\"ssl,omitempty\"`\n}\n\ntype SiteServerSSLInfo struct {\n\tIsSSL      *int32  `json:\"is_ssl,omitempty\"`\n\tFullChain  *string `json:\"full_chain,omitempty\"`\n\tPrivateKey *string `json:\"private_key,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/bunny/api_add_custom_certificate.go",
    "content": "package bunny\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype AddCustomCertificateRequest struct {\n\tHostname       string `json:\"Hostname\"`\n\tCertificate    string `json:\"Certificate\"`\n\tCertificateKey string `json:\"CertificateKey\"`\n}\n\nfunc (c *Client) AddCustomCertificate(pullZoneId string, req *AddCustomCertificateRequest) error {\n\treturn c.AddCustomCertificateWithContext(context.Background(), pullZoneId, req)\n}\n\nfunc (c *Client) AddCustomCertificateWithContext(ctx context.Context, pullZoneId string, req *AddCustomCertificateRequest) error {\n\tif pullZoneId == \"\" {\n\t\treturn fmt.Errorf(\"sdkerr: unset pullZoneId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf(\"/pullzone/%s/addCertificate\", url.PathEscape(pullZoneId)))\n\tif err != nil {\n\t\treturn err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tif _, err := c.doRequest(httpreq); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/bunny/client.go",
    "content": "package bunny\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(apiToken string) (*Client, error) {\n\tif apiToken == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiToken\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(\"https://api.bunny.net\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetHeader(\"AccessKey\", apiToken)\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/cachefly/api_create_certificate.go",
    "content": "package cachefly\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype CreateCertificateRequest struct {\n\tCertificate    *string `json:\"certificate,omitempty\"`\n\tCertificateKey *string `json:\"certificateKey,omitempty\"`\n\tPassword       *string `json:\"password,omitempty\"`\n}\n\ntype CreateCertificateResponse struct {\n\tsdkResponseBase\n\n\tId                string   `json:\"_id\"`\n\tSubjectCommonName string   `json:\"subjectCommonName\"`\n\tSubjectNames      []string `json:\"subjectNames\"`\n\tExpired           bool     `json:\"expired\"`\n\tExpiring          bool     `json:\"expiring\"`\n\tInUse             bool     `json:\"inUse\"`\n\tManaged           bool     `json:\"managed\"`\n\tServices          []string `json:\"services\"`\n\tDomains           []string `json:\"domains\"`\n\tNotBefore         string   `json:\"notBefore\"`\n\tNotAfter          string   `json:\"notAfter\"`\n\tCreatedAt         string   `json:\"createdAt\"`\n}\n\nfunc (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) {\n\treturn c.CreateCertificateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) CreateCertificateWithContext(ctx context.Context, req *CreateCertificateRequest) (*CreateCertificateResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/certificates\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &CreateCertificateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/cachefly/client.go",
    "content": "package cachefly\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(apiToken string) (*Client, error) {\n\tif apiToken == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiToken\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(\"https://api.cachefly.com/api/2.5\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetHeader(\"X-CF-Authorization\", \"Bearer \"+apiToken)\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/cachefly/types.go",
    "content": "package cachefly\n\ntype sdkResponse interface {\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tMessage *string `json:\"message,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n"
  },
  {
    "path": "pkg/sdk3rd/cdnfly/api_create_cert.go",
    "content": "package cdnfly\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype CreateCertRequest struct {\n\tName        *string `json:\"name,omitempty\"`\n\tDescription *string `json:\"des,omitempty\"`\n\tType        *string `json:\"type,omitempty\"`\n\tCert        *string `json:\"cert,omitempty\"`\n\tKey         *string `json:\"key,omitempty\"`\n}\n\ntype CreateCertResponse struct {\n\tsdkResponseBase\n\n\tData string `json:\"data\"`\n}\n\nfunc (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) {\n\treturn c.CreateCertWithContext(context.Background(), req)\n}\n\nfunc (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/certs\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &CreateCertResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/cdnfly/api_get_site.go",
    "content": "package cdnfly\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype GetSiteResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tId          int64  `json:\"id\"`\n\t\tName        string `json:\"name\"`\n\t\tDomain      string `json:\"domain\"`\n\t\tHttpsListen string `json:\"https_listen\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) GetSite(siteId string) (*GetSiteResponse, error) {\n\treturn c.GetSiteWithContext(context.Background(), siteId)\n}\n\nfunc (c *Client) GetSiteWithContext(ctx context.Context, siteId string) (*GetSiteResponse, error) {\n\tif siteId == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset siteId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf(\"/sites/%s\", url.PathEscape(siteId)))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &GetSiteResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/cdnfly/api_update_cert.go",
    "content": "package cdnfly\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype UpdateCertRequest struct {\n\tName        *string `json:\"name,omitempty\"`\n\tDescription *string `json:\"des,omitempty\"`\n\tType        *string `json:\"type,omitempty\"`\n\tCert        *string `json:\"cert,omitempty\"`\n\tKey         *string `json:\"key,omitempty\"`\n\tEnable      *bool   `json:\"enable,omitempty\"`\n}\n\ntype UpdateCertResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) UpdateCert(certId string, req *UpdateCertRequest) (*UpdateCertResponse, error) {\n\treturn c.UpdateCertWithContext(context.Background(), certId, req)\n}\n\nfunc (c *Client) UpdateCertWithContext(ctx context.Context, certId string, req *UpdateCertRequest) (*UpdateCertResponse, error) {\n\tif certId == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset certId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf(\"/certs/%s\", url.PathEscape(certId)))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateCertResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/cdnfly/api_update_site.go",
    "content": "package cdnfly\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype UpdateSiteRequest struct {\n\tHttpsListen *string `json:\"https_listen,omitempty\"`\n\tEnable      *bool   `json:\"enable,omitempty\"`\n}\n\ntype UpdateSiteResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) UpdateSite(siteId string, req *UpdateSiteRequest) (*UpdateSiteResponse, error) {\n\treturn c.UpdateSiteWithContext(context.Background(), siteId, req)\n}\n\nfunc (c *Client) UpdateSiteWithContext(ctx context.Context, siteId string, req *UpdateSiteRequest) (*UpdateSiteResponse, error) {\n\tif siteId == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset siteId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf(\"/sites/%s\", url.PathEscape(siteId)))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateSiteResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/cdnfly/client.go",
    "content": "package cdnfly\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl, apiKey, apiSecret string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif apiKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiKey\")\n\t}\n\tif apiSecret == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiSecret\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")+\"/v1\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetHeader(\"API-Key\", apiKey).\n\t\tSetHeader(\"API-Secret\", apiSecret)\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tcode := res.GetCode(); tcode != \"\" && tcode != \"0\" {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: code='%s', message='%s'\", tcode, res.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/cdnfly/types.go",
    "content": "package cdnfly\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"strconv\"\n)\n\ntype sdkResponse interface {\n\tGetCode() string\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    json.RawMessage `json:\"code\"`\n\tMessage string          `json:\"msg\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() string {\n\tif r.Code == nil {\n\t\treturn \"\"\n\t}\n\n\tdecoder := json.NewDecoder(bytes.NewReader(r.Code))\n\ttoken, err := decoder.Token()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tswitch t := token.(type) {\n\tcase string:\n\t\treturn t\n\tcase float64:\n\t\treturn strconv.FormatFloat(t, 'f', -1, 64)\n\tcase json.Number:\n\t\treturn t.String()\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\treturn r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n"
  },
  {
    "path": "pkg/sdk3rd/cpanel/api_ssl_install_ssl.go",
    "content": "package baishan\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype SSLInstallSSLRequest struct {\n\tDomain   *string `url:\"domain,omitempty\"`\n\tCert     *string `url:\"cert,omitempty\"`\n\tKey      *string `url:\"key,omitempty\"`\n\tCABundle *string `url:\"cabundle,omitempty\"`\n}\n\ntype SSLInstallSSLResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tUser                    string   `json:\"user\"`\n\t\tDomain                  string   `json:\"domain\"`\n\t\tExtraCertificateDomains []string `json:\"extra_certificate_domains,omitempty\"`\n\t\tWarningDomains          []string `json:\"warning_domains,omitempty\"`\n\t\tWorkingDomains          []string `json:\"working_domains,omitempty\"`\n\t\tCertId                  string   `json:\"cert_id\"`\n\t\tKeyId                   string   `json:\"key_id\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) SSLInstallSSL(req *SSLInstallSSLRequest) (*SSLInstallSSLResponse, error) {\n\treturn c.SSLInstallSSLWithContext(context.Background(), req)\n}\n\nfunc (c *Client) SSLInstallSSLWithContext(ctx context.Context, req *SSLInstallSSLRequest) (*SSLInstallSSLResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/SSL/install_ssl\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SSLInstallSSLResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/cpanel/client.go",
    "content": "package baishan\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl string, username, apiToken string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif username == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset username\")\n\t}\n\tif apiToken == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiToken\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")+\"/execute\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Authorization\", fmt.Sprintf(\"cpanel %s:%s\", username, apiToken)).\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent)\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tstatus := res.GetStatus(); tstatus == 0 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: status='%d', messages='%s', warnings='%s', errors='%s'\", tstatus, strings.Join(res.GetMessages(), \", \"), strings.Join(res.GetWarnings(), \", \"), strings.Join(res.GetErrors(), \", \"))\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/cpanel/types.go",
    "content": "package baishan\n\ntype sdkResponse interface {\n\tGetStatus() int\n\tGetMessages() []string\n\tGetWarnings() []string\n\tGetErrors() []string\n}\n\ntype sdkResponseBase struct {\n\tMetadata struct {\n\t\tTransformed int `json:\"transformed,omitempty\"`\n\t} `json:\"metadata\"`\n\tStatus   int      `json:\"status,omitempty\"`\n\tMessages []string `json:\"messages,omitempty\"`\n\tWarnings []string `json:\"warnings,omitempty\"`\n\tErrors   []string `json:\"errors,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetStatus() int {\n\treturn r.Status\n}\n\nfunc (r *sdkResponseBase) GetMessages() []string {\n\treturn r.Messages\n}\n\nfunc (r *sdkResponseBase) GetWarnings() []string {\n\treturn r.Warnings\n}\n\nfunc (r *sdkResponseBase) GetErrors() []string {\n\treturn r.Errors\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/ao/api_create_cert.go",
    "content": "package ao\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype CreateCertRequest struct {\n\tName  *string `json:\"name,omitempty\"`\n\tCerts *string `json:\"certs,omitempty\"`\n\tKey   *string `json:\"key,omitempty\"`\n}\n\ntype CreateCertResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tId int64 `json:\"id\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) {\n\treturn c.CreateCertWithContext(context.Background(), req)\n}\n\nfunc (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/ctapi/v1/accessone/cert/create\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &CreateCertResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/ao/api_get_domain_config.go",
    "content": "package ao\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype GetDomainConfigRequest struct {\n\tDomain      *string `json:\"domain,omitempty\"`\n\tProductCode *string `json:\"product_code,omitempty\"`\n}\n\ntype GetDomainConfigResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tDomain      string                          `json:\"domain\"`\n\t\tProductCode string                          `json:\"product_code\"`\n\t\tStatus      int32                           `json:\"status\"`\n\t\tAreaScope   int32                           `json:\"area_scope\"`\n\t\tCname       string                          `json:\"cname\"`\n\t\tOrigin      []*DomainOriginConfigWithWeight `json:\"origin,omitempty\"`\n\t\tHttpsStatus string                          `json:\"https_status\"`\n\t\tHttpsBasic  *DomainHttpsBasicConfig         `json:\"https_basic,omitempty\"`\n\t\tCertName    string                          `json:\"cert_name\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) GetDomainConfig(req *GetDomainConfigRequest) (*GetDomainConfigResponse, error) {\n\treturn c.GetDomainConfigWithContext(context.Background(), req)\n}\n\nfunc (c *Client) GetDomainConfigWithContext(ctx context.Context, req *GetDomainConfigRequest) (*GetDomainConfigResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/ctapi/v1/accessone/domain/config\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &GetDomainConfigResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/ao/api_list_certs.go",
    "content": "package ao\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype ListCertsRequest struct {\n\tPage      *int32 `json:\"page,omitempty\"       url:\"page,omitempty\"`\n\tPerPage   *int32 `json:\"per_page,omitempty\"   url:\"per_page,omitempty\"`\n\tUsageMode *int32 `json:\"usage_mode,omitempty\" url:\"usage_mode,omitempty\"`\n}\n\ntype ListCertsResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tResults      []*CertRecord `json:\"result,omitempty\"`\n\t\tPage         int32         `json:\"page,omitempty\"`\n\t\tPerPage      int32         `json:\"per_page,omitempty\"`\n\t\tTotalPage    int32         `json:\"total_page,omitempty\"`\n\t\tTotalRecords int32         `json:\"total_records,omitempty\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) ListCerts(req *ListCertsRequest) (*ListCertsResponse, error) {\n\treturn c.ListCertsWithContext(context.Background(), req)\n}\n\nfunc (c *Client) ListCertsWithContext(ctx context.Context, req *ListCertsRequest) (*ListCertsResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/ctapi/v1/accessone/cert/list\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ListCertsResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/ao/api_modify_domain_config.go",
    "content": "package ao\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype ModifyDomainConfigRequest struct {\n\tDomain      *string                 `json:\"domain,omitempty\"`\n\tProductCode *string                 `json:\"product_code,omitempty\"`\n\tOrigin      []*DomainOriginConfig   `json:\"origin,omitempty\"`\n\tHttpsStatus *string                 `json:\"https_status,omitempty\"`\n\tHttpsBasic  *DomainHttpsBasicConfig `json:\"https_basic,omitempty\"`\n\tCertName    *string                 `json:\"cert_name,omitempty\"`\n}\n\ntype ModifyDomainConfigResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) ModifyDomainConfig(req *ModifyDomainConfigRequest) (*ModifyDomainConfigResponse, error) {\n\treturn c.ModifyDomainConfigWithContext(context.Background(), req)\n}\n\nfunc (c *Client) ModifyDomainConfigWithContext(ctx context.Context, req *ModifyDomainConfigRequest) (*ModifyDomainConfigResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/ctapi/v1/scdn/domain/modify_config\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ModifyDomainConfigResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/ao/api_query_cert.go",
    "content": "package ao\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype QueryCertRequest struct {\n\tId        *int64  `json:\"id,omitempty\"         url:\"id,omitempty\"`\n\tName      *string `json:\"name,omitempty\"       url:\"name,omitempty\"`\n\tUsageMode *int32  `json:\"usage_mode,omitempty\" url:\"usage_mode,omitempty\"`\n}\n\ntype QueryCertResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tResult *CertDetail `json:\"result,omitempty\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) QueryCert(req *QueryCertRequest) (*QueryCertResponse, error) {\n\treturn c.QueryCertWithContext(context.Background(), req)\n}\n\nfunc (c *Client) QueryCertWithContext(ctx context.Context, req *QueryCertRequest) (*QueryCertResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/ctapi/v1/accessone/cert/query\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &QueryCertResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/ao/api_query_domains.go",
    "content": "package ao\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype QueryDomainsRequest struct {\n\tPage        *int32  `json:\"page,omitempty\"         url:\"page,omitempty\"`\n\tPageSize    *int32  `json:\"page_size,omitempty\"    url:\"page_size,omitempty\"`\n\tDomain      *string `json:\"domain,omitempty\"       url:\"domain,omitempty\"`\n\tProductCode *string `json:\"product_code,omitempty\" url:\"product_code,omitempty\"`\n\tStatus      *int32  `json:\"status,omitempty\"       url:\"status,omitempty\"`\n\tAreaScope   *int32  `json:\"area_scope,omitempty\"   url:\"area_scope,omitempty\"`\n}\n\ntype QueryDomainsResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tResults   []*DomainRecord `json:\"result,omitempty\"`\n\t\tPage      int32           `json:\"page,omitempty\"`\n\t\tPageSize  int32           `json:\"page_size,omitempty\"`\n\t\tPageCount int32           `json:\"page_count,omitempty\"`\n\t\tTotal     int32           `json:\"total,omitempty\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) QueryDomains(req *QueryDomainsRequest) (*QueryDomainsResponse, error) {\n\treturn c.QueryDomainsWithContext(context.Background(), req)\n}\n\nfunc (c *Client) QueryDomainsWithContext(ctx context.Context, req *QueryDomainsRequest) (*QueryDomainsResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/ctapi/v2/domain/query\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &QueryDomainsResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/ao/client.go",
    "content": "package ao\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/openapi\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst endpoint = \"https://accessone-global.ctapi.ctyun.cn\"\n\ntype Client struct {\n\tclient *openapi.Client\n}\n\nfunc NewClient(accessKeyId, secretAccessKey string) (*Client, error) {\n\tclient, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{client: client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\treturn c.client.NewRequest(method, path)\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\treturn c.client.DoRequest(req)\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tresp, err := c.client.DoRequestWithResult(req, res)\n\tif err == nil {\n\t\tif tcode := res.GetStatusCode(); tcode != \"\" && tcode != \"100000\" {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: code='%s', message='%s', errorCode='%s', errorMessage='%s'\", tcode, res.GetMessage(), res.GetMessage(), res.GetErrorMessage())\n\t\t}\n\t}\n\n\treturn resp, err\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/ao/types.go",
    "content": "package ao\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"strconv\"\n)\n\ntype sdkResponse interface {\n\tGetStatusCode() string\n\tGetMessage() string\n\tGetError() string\n\tGetErrorMessage() string\n}\n\ntype sdkResponseBase struct {\n\tStatusCode   json.RawMessage `json:\"statusCode,omitempty\"`\n\tMessage      *string         `json:\"message,omitempty\"`\n\tError        *string         `json:\"error,omitempty\"`\n\tErrorMessage *string         `json:\"errorMessage,omitempty\"`\n\tRequestId    *string         `json:\"requestId,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetStatusCode() string {\n\tif r.StatusCode == nil {\n\t\treturn \"\"\n\t}\n\n\tdecoder := json.NewDecoder(bytes.NewReader(r.StatusCode))\n\ttoken, err := decoder.Token()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tswitch t := token.(type) {\n\tcase string:\n\t\treturn t\n\tcase float64:\n\t\treturn strconv.FormatFloat(t, 'f', -1, 64)\n\tcase json.Number:\n\t\treturn t.String()\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nfunc (r *sdkResponseBase) GetError() string {\n\tif r.Error == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Error\n}\n\nfunc (r *sdkResponseBase) GetErrorMessage() string {\n\tif r.ErrorMessage == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.ErrorMessage\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype CertRecord struct {\n\tId          int64    `json:\"id\"`\n\tName        string   `json:\"name\"`\n\tCN          string   `json:\"cn\"`\n\tSANs        []string `json:\"sans\"`\n\tUsageMode   int32    `json:\"usage_mode\"`\n\tState       int32    `json:\"state\"`\n\tExpiresTime int64    `json:\"expires\"`\n\tIssueTime   int64    `json:\"issue\"`\n\tIssuer      string   `json:\"issuer\"`\n\tCreatedTime int64    `json:\"created\"`\n}\n\ntype CertDetail struct {\n\tCertRecord\n\tCerts string `json:\"certs\"`\n\tKey   string `json:\"key\"`\n}\n\ntype DomainRecord struct {\n\tDomain      string `json:\"domain\"`\n\tCname       string `json:\"cname\"`\n\tProductCode string `json:\"product_code\"`\n\tProductName string `json:\"product_name\"`\n\tStatus      int32  `json:\"status\"`\n\tAreaScope   int32  `json:\"area_scope\"`\n}\n\ntype DomainOriginConfig struct {\n\tOrigin string `json:\"origin\"`\n\tRole   string `json:\"role\"`\n\tWeight string `json:\"weight\"`\n}\n\ntype DomainOriginConfigWithWeight struct {\n\tOrigin string `json:\"origin\"`\n\tRole   string `json:\"role\"`\n\tWeight int32  `json:\"weight\"`\n}\n\ntype DomainHttpsBasicConfig struct {\n\tHttpsForce  string `json:\"https_force,omitempty\"`\n\tForceStatus string `json:\"force_status,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/cdn/api_create_cert.go",
    "content": "package cdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype CreateCertRequest struct {\n\tName  *string `json:\"name,omitempty\"`\n\tCerts *string `json:\"certs,omitempty\"`\n\tKey   *string `json:\"key,omitempty\"`\n}\n\ntype CreateCertResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tId int64 `json:\"id\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) {\n\treturn c.CreateCertWithContext(context.Background(), req)\n}\n\nfunc (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/v1/cert/creat-cert\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &CreateCertResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/cdn/api_query_cert_detail.go",
    "content": "package cdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype QueryCertDetailRequest struct {\n\tId        *int64  `json:\"id,omitempty\"         url:\"id,omitempty\"`\n\tName      *string `json:\"name,omitempty\"       url:\"name,omitempty\"`\n\tUsageMode *int32  `json:\"usage_mode,omitempty\" url:\"usage_mode,omitempty\"`\n}\n\ntype QueryCertDetailResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tResult *CertDetail `json:\"result,omitempty\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) QueryCertDetail(req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) {\n\treturn c.QueryCertDetailWithContext(context.Background(), req)\n}\n\nfunc (c *Client) QueryCertDetailWithContext(ctx context.Context, req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/v1/cert/query-cert-detail\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &QueryCertDetailResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/cdn/api_query_cert_list.go",
    "content": "package cdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype QueryCertListRequest struct {\n\tPage      *int32 `json:\"page,omitempty\"       url:\"page,omitempty\"`\n\tPerPage   *int32 `json:\"per_page,omitempty\"   url:\"per_page,omitempty\"`\n\tUsageMode *int32 `json:\"usage_mode,omitempty\" url:\"usage_mode,omitempty\"`\n}\n\ntype QueryCertListResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tResults      []*CertRecord `json:\"result,omitempty\"`\n\t\tPage         int32         `json:\"page,omitempty\"`\n\t\tPerPage      int32         `json:\"per_page,omitempty\"`\n\t\tTotalPage    int32         `json:\"total_page,omitempty\"`\n\t\tTotalRecords int32         `json:\"total_records,omitempty\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) QueryCertList(req *QueryCertListRequest) (*QueryCertListResponse, error) {\n\treturn c.QueryCertListWithContext(context.Background(), req)\n}\n\nfunc (c *Client) QueryCertListWithContext(ctx context.Context, req *QueryCertListRequest) (*QueryCertListResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/v1/cert/query-cert-list\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &QueryCertListResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/cdn/api_query_domain_detail.go",
    "content": "package cdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype QueryDomainDetailRequest struct {\n\tDomain        *string `json:\"domain,omitempty\"         url:\"domain,omitempty\"`\n\tProductCode   *string `json:\"product_code,omitempty\"   url:\"product_code,omitempty\"`\n\tFunctionNames *string `json:\"function_names,omitempty\" url:\"function_names,omitempty\"`\n}\n\ntype QueryDomainDetailResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *DomainDetail `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) QueryDomainDetail(req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) {\n\treturn c.QueryDomainDetailWithContext(context.Background(), req)\n}\n\nfunc (c *Client) QueryDomainDetailWithContext(ctx context.Context, req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/v1/domain/query-domain-detail\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &QueryDomainDetailResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/cdn/api_query_domain_list.go",
    "content": "package cdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype QueryDomainListRequest struct {\n\tPage        *int32  `json:\"page,omitempty\"         url:\"page,omitempty\"`\n\tPageSize    *int32  `json:\"page_size,omitempty\"    url:\"page_size,omitempty\"`\n\tDomain      *string `json:\"domain,omitempty\"       url:\"domain,omitempty\"`\n\tProductCode *string `json:\"product_code,omitempty\" url:\"product_code,omitempty\"`\n\tStatus      *int32  `json:\"status,omitempty\"       url:\"status,omitempty\"`\n\tAreaScope   *int32  `json:\"area_scope,omitempty\"   url:\"area_scope,omitempty\"`\n}\n\ntype QueryDomainListResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tResults   []*DomainRecord `json:\"result,omitempty\"`\n\t\tPage      int32           `json:\"page,omitempty\"`\n\t\tPageSize  int32           `json:\"page_size,omitempty\"`\n\t\tPageCount int32           `json:\"page_count,omitempty\"`\n\t\tTotal     int32           `json:\"total,omitempty\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) QueryDomainList(req *QueryDomainListRequest) (*QueryDomainListResponse, error) {\n\treturn c.QueryDomainListWithContext(context.Background(), req)\n}\n\nfunc (c *Client) QueryDomainListWithContext(ctx context.Context, req *QueryDomainListRequest) (*QueryDomainListResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/v1/domain/query-domain-list\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &QueryDomainListResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/cdn/api_update_domain.go",
    "content": "package cdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype UpdateDomainRequest struct {\n\tDomain      *string `json:\"domain,omitempty\"`\n\tHttpsStatus *string `json:\"https_status,omitempty\"`\n\tCertName    *string `json:\"cert_name,omitempty\"`\n}\n\ntype UpdateDomainResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) UpdateDomain(req *UpdateDomainRequest) (*UpdateDomainResponse, error) {\n\treturn c.UpdateDomainWithContext(context.Background(), req)\n}\n\nfunc (c *Client) UpdateDomainWithContext(ctx context.Context, req *UpdateDomainRequest) (*UpdateDomainResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/v1/domain/update-domain\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateDomainResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/cdn/client.go",
    "content": "package cdn\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/openapi\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst endpoint = \"https://ctcdn-global.ctapi.ctyun.cn\"\n\ntype Client struct {\n\tclient *openapi.Client\n}\n\nfunc NewClient(accessKeyId, secretAccessKey string) (*Client, error) {\n\tclient, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{client: client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\treturn c.client.NewRequest(method, path)\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\treturn c.client.DoRequest(req)\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tresp, err := c.client.DoRequestWithResult(req, res)\n\tif err == nil {\n\t\tif tcode := res.GetStatusCode(); tcode != \"\" && tcode != \"100000\" {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: code='%s', message='%s', errorCode='%s', errorMessage='%s'\", tcode, res.GetMessage(), res.GetMessage(), res.GetErrorMessage())\n\t\t}\n\t}\n\n\treturn resp, err\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/cdn/types.go",
    "content": "package cdn\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"strconv\"\n)\n\ntype sdkResponse interface {\n\tGetStatusCode() string\n\tGetMessage() string\n\tGetError() string\n\tGetErrorMessage() string\n}\n\ntype sdkResponseBase struct {\n\tStatusCode   json.RawMessage `json:\"statusCode,omitempty\"`\n\tMessage      *string         `json:\"message,omitempty\"`\n\tError        *string         `json:\"error,omitempty\"`\n\tErrorMessage *string         `json:\"errorMessage,omitempty\"`\n\tRequestId    *string         `json:\"requestId,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetStatusCode() string {\n\tif r.StatusCode == nil {\n\t\treturn \"\"\n\t}\n\n\tdecoder := json.NewDecoder(bytes.NewReader(r.StatusCode))\n\ttoken, err := decoder.Token()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tswitch t := token.(type) {\n\tcase string:\n\t\treturn t\n\tcase float64:\n\t\treturn strconv.FormatFloat(t, 'f', -1, 64)\n\tcase json.Number:\n\t\treturn t.String()\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nfunc (r *sdkResponseBase) GetError() string {\n\tif r.Error == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Error\n}\n\nfunc (r *sdkResponseBase) GetErrorMessage() string {\n\tif r.ErrorMessage == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.ErrorMessage\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype DomainRecord struct {\n\tDomain      string `json:\"domain\"`\n\tCname       string `json:\"cname\"`\n\tProductCode string `json:\"product_code\"`\n\tProductName string `json:\"product_name\"`\n\tAreaScope   int32  `json:\"area_scope\"`\n\tStatus      int32  `json:\"status\"`\n}\n\ntype DomainDetail struct {\n\tDomainRecord\n\tHttpsStatus string                  `json:\"https_status\"`\n\tHttpsBasic  *DomainHttpsBasicConfig `json:\"https_basic,omitempty\"`\n\tCertName    string                  `json:\"cert_name\"`\n\tSsl         string                  `json:\"ssl\"`\n\tSslStapling string                  `json:\"ssl_stapling\"`\n}\n\ntype DomainHttpsBasicConfig struct {\n\tHttpsForce     string `json:\"https_force\"`\n\tHttpForce      string `json:\"http_force\"`\n\tForceStatus    string `json:\"force_status\"`\n\tOriginProtocol string `json:\"origin_protocol\"`\n}\n\ntype CertRecord struct {\n\tId          int64    `json:\"id\"`\n\tName        string   `json:\"name\"`\n\tCN          string   `json:\"cn\"`\n\tSANs        []string `json:\"sans\"`\n\tUsageMode   int32    `json:\"usage_mode\"`\n\tState       int32    `json:\"state\"`\n\tExpiresTime int64    `json:\"expires\"`\n\tIssueTime   int64    `json:\"issue\"`\n\tIssuer      string   `json:\"issuer\"`\n\tCreatedTime int64    `json:\"created\"`\n}\n\ntype CertDetail struct {\n\tCertRecord\n\tCerts string `json:\"certs\"`\n\tKey   string `json:\"key\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/cms/api_get_certificate_list.go",
    "content": "package cms\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype GetCertificateListRequest struct {\n\tStatus   *string `json:\"status,omitempty\"`\n\tKeyword  *string `json:\"keyword,omitempty\"`\n\tPageNum  *int32  `json:\"pageNum,omitempty\"`\n\tPageSize *int32  `json:\"pageSize,omitempty\"`\n\tOrigin   *string `json:\"origin,omitempty\"`\n}\n\ntype GetCertificateListResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tList      []*CertificateRecord `json:\"list,omitempty\"`\n\t\tTotalSize int32                `json:\"totalSize,omitempty\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) GetCertificateList(req *GetCertificateListRequest) (*GetCertificateListResponse, error) {\n\treturn c.GetCertificateListWithContext(context.Background(), req)\n}\n\nfunc (c *Client) GetCertificateListWithContext(ctx context.Context, req *GetCertificateListRequest) (*GetCertificateListResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/v1/certificate/list\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &GetCertificateListResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/cms/api_upload_certificate.go",
    "content": "package cms\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype UploadCertificateRequest struct {\n\tName               *string `json:\"name,omitempty\"`\n\tCertificate        *string `json:\"certificate,omitempty\"`\n\tCertificateChain   *string `json:\"certificateChain,omitempty\"`\n\tPrivateKey         *string `json:\"privateKey,omitempty\"`\n\tEncryptionStandard *string `json:\"encryptionStandard,omitempty\"`\n\tEncCertificate     *string `json:\"encCertificate,omitempty\"`\n\tEncPrivateKey      *string `json:\"encPrivateKey,omitempty\"`\n}\n\ntype UploadCertificateResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) UploadCertificate(req *UploadCertificateRequest) (*UploadCertificateResponse, error) {\n\treturn c.UploadCertificateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) UploadCertificateWithContext(ctx context.Context, req *UploadCertificateRequest) (*UploadCertificateResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/v1/certificate/upload\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UploadCertificateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/cms/client.go",
    "content": "package cms\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/openapi\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst endpoint = \"https://ccms-global.ctapi.ctyun.cn\"\n\ntype Client struct {\n\tclient *openapi.Client\n}\n\nfunc NewClient(accessKeyId, secretAccessKey string) (*Client, error) {\n\tclient, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{client: client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\treturn c.client.NewRequest(method, path)\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\treturn c.client.DoRequest(req)\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tresp, err := c.client.DoRequestWithResult(req, res)\n\tif err == nil {\n\t\tstatusCode := res.GetStatusCode()\n\t\terrorCode := res.GetError()\n\t\tif (statusCode != \"\" && statusCode != \"200\") || errorCode != \"\" {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: code='%s', message='%s', errorCode='%s', errorMessage='%s'\", statusCode, res.GetMessage(), res.GetMessage(), res.GetErrorMessage())\n\t\t}\n\t}\n\n\treturn resp, err\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/cms/types.go",
    "content": "package cms\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"strconv\"\n)\n\ntype sdkResponse interface {\n\tGetStatusCode() string\n\tGetMessage() string\n\tGetError() string\n\tGetErrorMessage() string\n}\n\ntype sdkResponseBase struct {\n\tStatusCode   json.RawMessage `json:\"statusCode,omitempty\"`\n\tMessage      *string         `json:\"message,omitempty\"`\n\tError        *string         `json:\"error,omitempty\"`\n\tErrorMessage *string         `json:\"errorMessage,omitempty\"`\n\tRequestId    *string         `json:\"requestId,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetStatusCode() string {\n\tif r.StatusCode == nil {\n\t\treturn \"\"\n\t}\n\n\tdecoder := json.NewDecoder(bytes.NewReader(r.StatusCode))\n\ttoken, err := decoder.Token()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tswitch t := token.(type) {\n\tcase string:\n\t\treturn t\n\tcase float64:\n\t\treturn strconv.FormatFloat(t, 'f', -1, 64)\n\tcase json.Number:\n\t\treturn t.String()\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nfunc (r *sdkResponseBase) GetError() string {\n\tif r.Error == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Error\n}\n\nfunc (r *sdkResponseBase) GetErrorMessage() string {\n\tif r.ErrorMessage == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.ErrorMessage\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype CertificateRecord struct {\n\tId                  string `json:\"id\"`\n\tOrigin              string `json:\"origin\"`\n\tType                string `json:\"type\"`\n\tResourceId          string `json:\"resourceId\"`\n\tResourceType        string `json:\"resourceType\"`\n\tCertificateId       string `json:\"certificateId\"`\n\tCertificateMode     string `json:\"certificateMode\"`\n\tName                string `json:\"name\"`\n\tStatus              string `json:\"status\"`\n\tDetailStatus        string `json:\"detailStatus\"`\n\tManagedStatus       string `json:\"managedStatus\"`\n\tFingerprint         string `json:\"fingerprint\"`\n\tIssueTime           string `json:\"issueTime\"`\n\tExpireTime          string `json:\"expireTime\"`\n\tDomainType          string `json:\"domainType\"`\n\tDomainName          string `json:\"domainName\"`\n\tEncryptionStandard  string `json:\"encryptionStandard\"`\n\tEncryptionAlgorithm string `json:\"encryptionAlgorithm\"`\n\tCreateTime          string `json:\"createTime\"`\n\tUpdateTime          string `json:\"updateTime\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/dns/api_add_record.go",
    "content": "package dns\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype AddRecordRequest struct {\n\tDomain   *string `json:\"domain,omitempty\"`\n\tHost     *string `json:\"host,omitempty\"`\n\tType     *string `json:\"type,omitempty\"`\n\tLineCode *string `json:\"lineCode,omitempty\"`\n\tValue    *string `json:\"value,omitempty\"`\n\tTTL      *int32  `json:\"ttl,omitempty\"`\n\tState    *int32  `json:\"state,omitempty\"`\n\tRemark   *string `json:\"remark\"`\n}\n\ntype AddRecordResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tRecordId int32 `json:\"recordId\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) AddRecord(req *AddRecordRequest) (*AddRecordResponse, error) {\n\treturn c.AddRecordWithContext(context.Background(), req)\n}\n\nfunc (c *Client) AddRecordWithContext(ctx context.Context, req *AddRecordRequest) (*AddRecordResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/v2/addRecord\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &AddRecordResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/dns/api_delete_record.go",
    "content": "package dns\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype DeleteRecordRequest struct {\n\tRecordId *int32 `json:\"recordId,omitempty\"`\n}\n\ntype DeleteRecordResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) DeleteRecord(req *DeleteRecordRequest) (*DeleteRecordResponse, error) {\n\treturn c.DeleteRecordWithContext(context.Background(), req)\n}\n\nfunc (c *Client) DeleteRecordWithContext(ctx context.Context, req *DeleteRecordRequest) (*DeleteRecordResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/v2/deleteRecord\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &DeleteRecordResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/dns/api_query_record_list.go",
    "content": "package dns\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype QueryRecordListRequest struct {\n\tDomain   *string `json:\"domain,omitempty\"   url:\"domain,omitempty\"`\n\tHost     *string `json:\"host,omitempty\"     url:\"host,omitempty\"`\n\tType     *string `json:\"type,omitempty\"     url:\"type,omitempty\"`\n\tLineCode *string `json:\"lineCode,omitempty\" url:\"lineCode,omitempty\"`\n\tValue    *string `json:\"value,omitempty\"    url:\"value,omitempty\"`\n\tState    *int32  `json:\"state,omitempty\"    url:\"state,omitempty\"`\n}\n\ntype QueryRecordListResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tRecords []*DnsRecord `json:\"records,omitempty\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) QueryRecordList(req *QueryRecordListRequest) (*QueryRecordListResponse, error) {\n\treturn c.QueryRecordListWithContext(context.Background(), req)\n}\n\nfunc (c *Client) QueryRecordListWithContext(ctx context.Context, req *QueryRecordListRequest) (*QueryRecordListResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/v2/queryRecordList\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &QueryRecordListResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/dns/api_update_record.go",
    "content": "package dns\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype UpdateRecordRequest struct {\n\tRecordId *int32  `json:\"recordId,omitempty\"`\n\tDomain   *string `json:\"domain,omitempty\"`\n\tHost     *string `json:\"host,omitempty\"`\n\tType     *string `json:\"type,omitempty\"`\n\tLineCode *string `json:\"lineCode,omitempty\"`\n\tValue    *string `json:\"value,omitempty\"`\n\tTTL      *int32  `json:\"ttl,omitempty\"`\n\tState    *int32  `json:\"state,omitempty\"`\n\tRemark   *string `json:\"remark\"`\n}\n\ntype UpdateRecordResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tRecordId int32 `json:\"recordId\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) UpdateRecord(req *UpdateRecordRequest) (*UpdateRecordResponse, error) {\n\treturn c.UpdateRecordWithContext(context.Background(), req)\n}\n\nfunc (c *Client) UpdateRecordWithContext(ctx context.Context, req *UpdateRecordRequest) (*UpdateRecordResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/v2/updateRecord\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateRecordResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/dns/client.go",
    "content": "package dns\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/openapi\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst endpoint = \"https://smartdns-global.ctapi.ctyun.cn\"\n\ntype Client struct {\n\tclient *openapi.Client\n}\n\nfunc NewClient(accessKeyId, secretAccessKey string) (*Client, error) {\n\tclient, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{client: client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\treturn c.client.NewRequest(method, path)\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\treturn c.client.DoRequest(req)\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tresp, err := c.client.DoRequestWithResult(req, res)\n\tif err == nil {\n\t\tstatusCode := res.GetStatusCode()\n\t\terrorCode := res.GetError()\n\t\tif (statusCode != \"\" && statusCode != \"200\") || errorCode != \"\" {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: code='%s', message='%s', errorCode='%s', errorMessage='%s'\", statusCode, res.GetMessage(), res.GetMessage(), res.GetErrorMessage())\n\t\t}\n\t}\n\n\treturn resp, err\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/dns/types.go",
    "content": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"strconv\"\n)\n\ntype sdkResponse interface {\n\tGetStatusCode() string\n\tGetMessage() string\n\tGetError() string\n\tGetErrorMessage() string\n}\n\ntype sdkResponseBase struct {\n\tStatusCode   json.RawMessage `json:\"statusCode,omitempty\"`\n\tMessage      *string         `json:\"message,omitempty\"`\n\tError        *string         `json:\"error,omitempty\"`\n\tErrorMessage *string         `json:\"errorMessage,omitempty\"`\n\tRequestId    *string         `json:\"requestId,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetStatusCode() string {\n\tif r.StatusCode == nil {\n\t\treturn \"\"\n\t}\n\n\tdecoder := json.NewDecoder(bytes.NewReader(r.StatusCode))\n\ttoken, err := decoder.Token()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tswitch t := token.(type) {\n\tcase string:\n\t\treturn t\n\tcase float64:\n\t\treturn strconv.FormatFloat(t, 'f', -1, 64)\n\tcase json.Number:\n\t\treturn t.String()\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nfunc (r *sdkResponseBase) GetError() string {\n\tif r.Error == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Error\n}\n\nfunc (r *sdkResponseBase) GetErrorMessage() string {\n\tif r.ErrorMessage == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.ErrorMessage\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype DnsRecord struct {\n\tRecordId int32  `json:\"recordId\"`\n\tHost     string `json:\"host\"`\n\tType     string `json:\"type\"`\n\tLineCode string `json:\"lineCode\"`\n\tValue    string `json:\"value\"`\n\tTTL      int32  `json:\"ttl\"`\n\tState    int32  `json:\"state\"`\n\tRemark   string `json:\"remark\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/elb/api_create_certificate.go",
    "content": "package elb\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype CreateCertificateRequest struct {\n\tClientToken *string `json:\"clientToken,omitempty\"`\n\tRegionID    *string `json:\"regionID,omitempty\"`\n\tName        *string `json:\"name,omitempty\"`\n\tDescription *string `json:\"description,omitempty\"`\n\tType        *string `json:\"type,omitempty\"`\n\tCertificate *string `json:\"certificate,omitempty\"`\n\tPrivateKey  *string `json:\"privateKey,omitempty\"`\n}\n\ntype CreateCertificateResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tID string `json:\"id\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) {\n\treturn c.CreateCertificateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) CreateCertificateWithContext(ctx context.Context, req *CreateCertificateRequest) (*CreateCertificateResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/v4/elb/create-certificate\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &CreateCertificateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/elb/api_list_certificates.go",
    "content": "package elb\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype ListCertificatesRequest struct {\n\tClientToken *string `json:\"clientToken,omitempty\" url:\"clientToken,omitempty\"`\n\tRegionID    *string `json:\"regionID,omitempty\"    url:\"regionID,omitempty\"`\n\tIDs         *string `json:\"IDs,omitempty\"         url:\"IDs,omitempty\"`\n\tName        *string `json:\"name,omitempty\"        url:\"name,omitempty\"`\n\tType        *string `json:\"type,omitempty\"        url:\"type,omitempty\"`\n}\n\ntype ListCertificatesResponse struct {\n\tsdkResponseBase\n\n\tReturnObj []*CertificateRecord `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) ListCertificates(req *ListCertificatesRequest) (*ListCertificatesResponse, error) {\n\treturn c.ListCertificatesWithContext(context.Background(), req)\n}\n\nfunc (c *Client) ListCertificatesWithContext(ctx context.Context, req *ListCertificatesRequest) (*ListCertificatesResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/v4/elb/list-certificate\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ListCertificatesResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/elb/api_list_listeners.go",
    "content": "package elb\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype ListListenersRequest struct {\n\tClientToken     *string `json:\"clientToken,omitempty\"     url:\"clientToken,omitempty\"`\n\tRegionID        *string `json:\"regionID,omitempty\"        url:\"regionID,omitempty\"`\n\tProjectID       *string `json:\"projectID,omitempty\"       url:\"projectID,omitempty\"`\n\tIDs             *string `json:\"IDs,omitempty\"             url:\"IDs,omitempty\"`\n\tName            *string `json:\"name,omitempty\"            url:\"name,omitempty\"`\n\tLoadBalancerID  *string `json:\"loadBalancerID,omitempty\"  url:\"loadBalancerID,omitempty\"`\n\tAccessControlID *string `json:\"accessControlID,omitempty\" url:\"accessControlID,omitempty\"`\n}\n\ntype ListListenersResponse struct {\n\tsdkResponseBase\n\n\tReturnObj []*ListenerRecord `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) ListListeners(req *ListListenersRequest) (*ListListenersResponse, error) {\n\treturn c.ListListenersWithContext(context.Background(), req)\n}\n\nfunc (c *Client) ListListenersWithContext(ctx context.Context, req *ListListenersRequest) (*ListListenersResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/v4/elb/list-listener\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ListListenersResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/elb/api_show_listener.go",
    "content": "package elb\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype ShowListenerRequest struct {\n\tClientToken *string `json:\"clientToken,omitempty\" url:\"clientToken,omitempty\"`\n\tRegionID    *string `json:\"regionID,omitempty\"    url:\"regionID,omitempty\"`\n\tListenerID  *string `json:\"listenerID,omitempty\"  url:\"listenerID,omitempty\"`\n}\n\ntype ShowListenerResponse struct {\n\tsdkResponseBase\n\n\tReturnObj []*ListenerRecord `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) ShowListener(req *ShowListenerRequest) (*ShowListenerResponse, error) {\n\treturn c.ShowListenerWithContext(context.Background(), req)\n}\n\nfunc (c *Client) ShowListenerWithContext(ctx context.Context, req *ShowListenerRequest) (*ShowListenerResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/v4/elb/show-listener\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ShowListenerResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/elb/api_update_listener.go",
    "content": "package elb\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype UpdateListenerRequest struct {\n\tClientToken         *string `json:\"clientToken,omitempty\"`\n\tRegionID            *string `json:\"regionID,omitempty\"`\n\tListenerID          *string `json:\"listenerID,omitempty\"`\n\tName                *string `json:\"name,omitempty\"`\n\tDescription         *string `json:\"description,omitempty\"`\n\tCertificateID       *string `json:\"certificateID,omitempty\"`\n\tCaEnabled           *bool   `json:\"caEnabled,omitempty\"`\n\tClientCertificateID *string `json:\"clientCertificateID,omitempty\"`\n}\n\ntype UpdateListenerResponse struct {\n\tsdkResponseBase\n\n\tReturnObj []*ListenerRecord `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) UpdateListener(req *UpdateListenerRequest) (*UpdateListenerResponse, error) {\n\treturn c.UpdateListenerWithContext(context.Background(), req)\n}\n\nfunc (c *Client) UpdateListenerWithContext(ctx context.Context, req *UpdateListenerRequest) (*UpdateListenerResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/v4/elb/update-listener\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateListenerResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/elb/client.go",
    "content": "package elb\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/openapi\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst endpoint = \"https://ctelb-global.ctapi.ctyun.cn\"\n\ntype Client struct {\n\tclient *openapi.Client\n}\n\nfunc NewClient(accessKeyId, secretAccessKey string) (*Client, error) {\n\tclient, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{client: client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\treturn c.client.NewRequest(method, path)\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\treturn c.client.DoRequest(req)\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tresp, err := c.client.DoRequestWithResult(req, res)\n\tif err == nil {\n\t\tstatusCode := res.GetStatusCode()\n\t\terrorCode := res.GetError()\n\t\tif (statusCode != \"\" && statusCode != \"200\" && statusCode != \"800\") || (errorCode != \"\" && errorCode != \"SUCCESS\") {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: code='%s', message='%s', errorCode='%s', description='%s'\", statusCode, res.GetMessage(), res.GetMessage(), res.GetDescription())\n\t\t}\n\t}\n\n\treturn resp, err\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/elb/types.go",
    "content": "package elb\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"strconv\"\n)\n\ntype sdkResponse interface {\n\tGetStatusCode() string\n\tGetMessage() string\n\tGetError() string\n\tGetDescription() string\n}\n\ntype sdkResponseBase struct {\n\tStatusCode  json.RawMessage `json:\"statusCode,omitempty\"`\n\tMessage     *string         `json:\"message,omitempty\"`\n\tError       *string         `json:\"error,omitempty\"`\n\tDescription *string         `json:\"description,omitempty\"`\n\tRequestId   *string         `json:\"requestId,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetStatusCode() string {\n\tif r.StatusCode == nil {\n\t\treturn \"\"\n\t}\n\n\tdecoder := json.NewDecoder(bytes.NewReader(r.StatusCode))\n\ttoken, err := decoder.Token()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tswitch t := token.(type) {\n\tcase string:\n\t\treturn t\n\tcase float64:\n\t\treturn strconv.FormatFloat(t, 'f', -1, 64)\n\tcase json.Number:\n\t\treturn t.String()\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nfunc (r *sdkResponseBase) GetError() string {\n\tif r.Error == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Error\n}\n\nfunc (r *sdkResponseBase) GetDescription() string {\n\tif r.Description == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Description\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype CertificateRecord struct {\n\tID          string `json:\"ID\"`\n\tRegionID    string `json:\"regionID\"`\n\tAzName      string `json:\"azName\"`\n\tProjectID   string `json:\"projectID\"`\n\tName        string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tType        string `json:\"type\"`\n\tCertificate string `json:\"certificate\"`\n\tPrivateKey  string `json:\"privateKey\"`\n\tStatus      string `json:\"status\"`\n\tCreatedTime string `json:\"createdTime\"`\n\tUpdatedTime string `json:\"updatedTime\"`\n}\n\ntype ListenerRecord struct {\n\tID                  string `json:\"ID\"`\n\tRegionID            string `json:\"regionID\"`\n\tAzName              string `json:\"azName\"`\n\tProjectID           string `json:\"projectID\"`\n\tName                string `json:\"name\"`\n\tDescription         string `json:\"description\"`\n\tLoadBalancerID      string `json:\"loadBalancerID\"`\n\tProtocol            string `json:\"protocol\"`\n\tProtocolPort        int32  `json:\"protocolPort\"`\n\tCertificateID       string `json:\"certificateID,omitempty\"`\n\tCaEnabled           bool   `json:\"caEnabled\"`\n\tClientCertificateID string `json:\"clientCertificateID,omitempty\"`\n\tStatus              string `json:\"status\"`\n\tCreatedTime         string `json:\"createdTime\"`\n\tUpdatedTime         string `json:\"updatedTime\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/faas/api_get_custom_domain.go",
    "content": "package faas\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype GetCustomDomainRequest struct {\n\tRegionId   *string `json:\"-\"                    url:\"-\"`\n\tDomainName *string `json:\"domainName,omitempty\" url:\"-\"`\n\tCnameCheck *bool   `json:\"cnameCheck,omitempty\" url:\"cnameCheck,omitempty\"`\n}\n\ntype GetCustomDomainResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *CustomDomainRecord `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) GetCustomDomain(req *GetCustomDomainRequest) (*GetCustomDomainResponse, error) {\n\treturn c.GetCustomDomainWithContext(context.Background(), req)\n}\n\nfunc (c *Client) GetCustomDomainWithContext(ctx context.Context, req *GetCustomDomainRequest) (*GetCustomDomainResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf(\"/openapi/v1/domains/customdomains/%s\", url.PathEscape(*req.DomainName)))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tif req.RegionId != nil {\n\t\t\thttpreq.SetHeader(\"regionId\", *req.RegionId)\n\t\t}\n\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &GetCustomDomainResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/faas/api_update_custom_domain.go",
    "content": "package faas\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype UpdateCustomDomainRequest struct {\n\tRegionId    *string                  `json:\"-\"`\n\tDomainName  *string                  `json:\"domainName,omitempty\"`\n\tProtocol    *string                  `json:\"protocol,omitempty\"`\n\tAuthConfig  *CustomDomainAuthConfig  `json:\"authConfig,omitempty\"`\n\tCertConfig  *CustomDomainCertConfig  `json:\"certConfig,omitempty\"`\n\tRouteConfig *CustomDomainRouteConfig `json:\"routeConfig,omitempty\"`\n}\n\ntype UpdateCustomDomainResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *CustomDomainRecord `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) UpdateCustomDomain(req *UpdateCustomDomainRequest) (*UpdateCustomDomainResponse, error) {\n\treturn c.UpdateCustomDomainWithContext(context.Background(), req)\n}\n\nfunc (c *Client) UpdateCustomDomainWithContext(ctx context.Context, req *UpdateCustomDomainRequest) (*UpdateCustomDomainResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf(\"/openapi/v1/domains/customdomains/%s\", url.PathEscape(*req.DomainName)))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tif req.RegionId != nil {\n\t\t\thttpreq.SetHeader(\"regionId\", *req.RegionId)\n\t\t}\n\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateCustomDomainResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/faas/client.go",
    "content": "package faas\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/openapi\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst endpoint = \"https://cf-global.ctapi.ctyun.cn\"\n\ntype Client struct {\n\tclient *openapi.Client\n}\n\nfunc NewClient(accessKeyId, secretAccessKey string) (*Client, error) {\n\tclient, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{client: client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\treturn c.client.NewRequest(method, path)\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\treturn c.client.DoRequest(req)\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tresp, err := c.client.DoRequestWithResult(req, res)\n\tif err == nil {\n\t\tif tcode := res.GetStatusCode(); tcode != \"\" && tcode != \"0\" {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: code='%s', message='%s', errorCode='%s', errorMessage='%s'\", tcode, res.GetMessage(), res.GetMessage(), res.GetErrorMessage())\n\t\t}\n\t}\n\n\treturn resp, err\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/faas/types.go",
    "content": "package faas\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"strconv\"\n)\n\ntype sdkResponse interface {\n\tGetStatusCode() string\n\tGetMessage() string\n\tGetError() string\n\tGetErrorMessage() string\n}\n\ntype sdkResponseBase struct {\n\tStatusCode   json.RawMessage `json:\"statusCode,omitempty\"`\n\tMessage      *string         `json:\"message,omitempty\"`\n\tError        *string         `json:\"error,omitempty\"`\n\tErrorMessage *string         `json:\"errorMessage,omitempty\"`\n\tRequestId    *string         `json:\"requestId,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetStatusCode() string {\n\tif r.StatusCode == nil {\n\t\treturn \"\"\n\t}\n\n\tdecoder := json.NewDecoder(bytes.NewReader(r.StatusCode))\n\ttoken, err := decoder.Token()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tswitch t := token.(type) {\n\tcase string:\n\t\treturn t\n\tcase float64:\n\t\treturn strconv.FormatFloat(t, 'f', -1, 64)\n\tcase json.Number:\n\t\treturn t.String()\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nfunc (r *sdkResponseBase) GetError() string {\n\tif r.Error == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Error\n}\n\nfunc (r *sdkResponseBase) GetErrorMessage() string {\n\tif r.ErrorMessage == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.ErrorMessage\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype CustomDomainRecord struct {\n\tDomainName   string                   `json:\"domainName\"`\n\tProtocol     string                   `json:\"protocol\"`\n\tAuthConfig   *CustomDomainAuthConfig  `json:\"authConfig,omitempty\"`\n\tCertConfig   *CustomDomainCertConfig  `json:\"certConfig,omitempty\"`\n\tRouteConfig  *CustomDomainRouteConfig `json:\"routeConfig,omitempty\"`\n\tDomainStatus string                   `json:\"domainStatus\"`\n\tCnameValid   bool                     `json:\"cnameValid\"`\n\tCreatedAt    string                   `json:\"createdAt\"`\n\tUpdatedAt    string                   `json:\"updatedAt\"`\n}\n\ntype CustomDomainAuthConfig struct {\n\tAuthType  string                     `json:\"authType\"`\n\tJwtConfig *CustomDomainAuthJwtConfig `json:\"jwtConfig,omitempty\"`\n}\n\ntype CustomDomainAuthJwtConfig struct {\n\tJwks        string                              `json:\"jwks\"`\n\tTokenConfig *CustomDomainAuthJwtTokenConfig     `json:\"tokenConfig,omitempty\"`\n\tClaimTrans  []*CustomDomainAuthJwtClaimTran     `json:\"claimTrans,omitempty\"`\n\tMatchMode   *CustomDomainAuthJwtMatchModeConfig `json:\"matchMode,omitempty\"`\n}\n\ntype CustomDomainAuthJwtClaimTran struct {\n\tClaimName     string `json:\"claimName\"`\n\tTargetName    string `json:\"targetName\"`\n\tTransLocation string `json:\"transLocation\"`\n}\n\ntype CustomDomainAuthJwtTokenConfig struct {\n\tLocation     string  `json:\"location\"`\n\tName         string  `json:\"name\"`\n\tRemovePrefix *string `json:\"removePrefix,omitempty\"`\n}\n\ntype CustomDomainAuthJwtMatchModeConfig struct {\n\tMode string   `json:\"mode\"`\n\tPath []string `json:\"path\"`\n}\n\ntype CustomDomainCertConfig struct {\n\tCertName    string `json:\"certName\"`\n\tCertificate string `json:\"certificate\"`\n\tPrivateKey  string `json:\"privateKey\"`\n}\n\ntype CustomDomainRouteConfig struct {\n\tRoutes []*CustomDomainRoutePathConfig `json:\"routes\"`\n}\n\ntype CustomDomainRoutePathConfig struct {\n\tEnableJwt          int32    `json:\"enableJwt\"`\n\tFunctionId         int64    `json:\"functionId\"`\n\tFunctionName       string   `json:\"functionName\"`\n\tFunctionUniqueName string   `json:\"functionUniqueName\"`\n\tMethods            []string `json:\"methods\"`\n\tPath               string   `json:\"path\"`\n\tQualifier          string   `json:\"qualifier,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/icdn/api_create_cert.go",
    "content": "package icdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype CreateCertRequest struct {\n\tName  *string `json:\"name,omitempty\"`\n\tCerts *string `json:\"certs,omitempty\"`\n\tKey   *string `json:\"key,omitempty\"`\n}\n\ntype CreateCertResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tId int64 `json:\"id\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) {\n\treturn c.CreateCertWithContext(context.Background(), req)\n}\n\nfunc (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/v1/cert/creat-cert\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &CreateCertResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/icdn/api_query_cert_detail.go",
    "content": "package icdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype QueryCertDetailRequest struct {\n\tId        *int64  `json:\"id,omitempty\"         url:\"id,omitempty\"`\n\tName      *string `json:\"name,omitempty\"       url:\"name,omitempty\"`\n\tUsageMode *int32  `json:\"usage_mode,omitempty\" url:\"usage_mode,omitempty\"`\n}\n\ntype QueryCertDetailResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tResult *CertDetail `json:\"result,omitempty\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) QueryCertDetail(req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) {\n\treturn c.QueryCertDetailWithContext(context.Background(), req)\n}\n\nfunc (c *Client) QueryCertDetailWithContext(ctx context.Context, req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/v1/cert/query-cert-detail\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &QueryCertDetailResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/icdn/api_query_cert_list.go",
    "content": "package icdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype QueryCertListRequest struct {\n\tPage      *int32 `json:\"page,omitempty\"       url:\"page,omitempty\"`\n\tPerPage   *int32 `json:\"per_page,omitempty\"   url:\"per_page,omitempty\"`\n\tUsageMode *int32 `json:\"usage_mode,omitempty\" url:\"usage_mode,omitempty\"`\n}\n\ntype QueryCertListResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tResults      []*CertRecord `json:\"result,omitempty\"`\n\t\tPage         int32         `json:\"page,omitempty\"`\n\t\tPerPage      int32         `json:\"per_page,omitempty\"`\n\t\tTotalPage    int32         `json:\"total_page,omitempty\"`\n\t\tTotalRecords int32         `json:\"total_records,omitempty\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) QueryCertList(req *QueryCertListRequest) (*QueryCertListResponse, error) {\n\treturn c.QueryCertListWithContext(context.Background(), req)\n}\n\nfunc (c *Client) QueryCertListWithContext(ctx context.Context, req *QueryCertListRequest) (*QueryCertListResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/v1/cert/query-cert-list\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &QueryCertListResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/icdn/api_query_domain_detail.go",
    "content": "package icdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype QueryDomainDetailRequest struct {\n\tDomain        *string `json:\"domain,omitempty\"         url:\"domain,omitempty\"`\n\tProductCode   *string `json:\"product_code,omitempty\"   url:\"product_code,omitempty\"`\n\tFunctionNames *string `json:\"function_names,omitempty\" url:\"function_names,omitempty\"`\n}\n\ntype QueryDomainDetailResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *DomainDetail `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) QueryDomainDetail(req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) {\n\treturn c.QueryDomainDetailWithContext(context.Background(), req)\n}\n\nfunc (c *Client) QueryDomainDetailWithContext(ctx context.Context, req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/v1/domain/query-domain-detail\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &QueryDomainDetailResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/icdn/api_query_domain_list.go",
    "content": "package icdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype QueryDomainListRequest struct {\n\tPage        *int32  `json:\"page,omitempty\"         url:\"page,omitempty\"`\n\tPageSize    *int32  `json:\"page_size,omitempty\"    url:\"page_size,omitempty\"`\n\tDomain      *string `json:\"domain,omitempty\"       url:\"domain,omitempty\"`\n\tProductCode *string `json:\"product_code,omitempty\" url:\"product_code,omitempty\"`\n\tStatus      *int32  `json:\"status,omitempty\"       url:\"status,omitempty\"`\n\tAreaScope   *int32  `json:\"area_scope,omitempty\"   url:\"area_scope,omitempty\"`\n}\n\ntype QueryDomainListResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tResults   []*DomainRecord `json:\"result,omitempty\"`\n\t\tPage      int32           `json:\"page,omitempty\"`\n\t\tPageSize  int32           `json:\"page_size,omitempty\"`\n\t\tPageCount int32           `json:\"page_count,omitempty\"`\n\t\tTotal     int32           `json:\"total,omitempty\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) QueryDomainList(req *QueryDomainListRequest) (*QueryDomainListResponse, error) {\n\treturn c.QueryDomainListWithContext(context.Background(), req)\n}\n\nfunc (c *Client) QueryDomainListWithContext(ctx context.Context, req *QueryDomainListRequest) (*QueryDomainListResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/v1/domain/query-domain-list\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &QueryDomainListResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/icdn/api_update_domain.go",
    "content": "package icdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype UpdateDomainRequest struct {\n\tDomain      *string `json:\"domain,omitempty\"`\n\tHttpsStatus *string `json:\"https_status,omitempty\"`\n\tCertName    *string `json:\"cert_name,omitempty\"`\n}\n\ntype UpdateDomainResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) UpdateDomain(req *UpdateDomainRequest) (*UpdateDomainResponse, error) {\n\treturn c.UpdateDomainWithContext(context.Background(), req)\n}\n\nfunc (c *Client) UpdateDomainWithContext(ctx context.Context, req *UpdateDomainRequest) (*UpdateDomainResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/v1/domain/update-domain\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateDomainResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/icdn/client.go",
    "content": "package icdn\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/openapi\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst endpoint = \"https://icdn-global.ctapi.ctyun.cn\"\n\ntype Client struct {\n\tclient *openapi.Client\n}\n\nfunc NewClient(accessKeyId, secretAccessKey string) (*Client, error) {\n\tclient, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{client: client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\treturn c.client.NewRequest(method, path)\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\treturn c.client.DoRequest(req)\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tresp, err := c.client.DoRequestWithResult(req, res)\n\tif err == nil {\n\t\tif tcode := res.GetStatusCode(); tcode != \"\" && tcode != \"100000\" {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: code='%s', message='%s', errorCode='%s', errorMessage='%s'\", tcode, res.GetMessage(), res.GetMessage(), res.GetErrorMessage())\n\t\t}\n\t}\n\n\treturn resp, err\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/icdn/types.go",
    "content": "package icdn\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"strconv\"\n)\n\ntype sdkResponse interface {\n\tGetStatusCode() string\n\tGetMessage() string\n\tGetError() string\n\tGetErrorMessage() string\n}\n\ntype sdkResponseBase struct {\n\tStatusCode   json.RawMessage `json:\"statusCode,omitempty\"`\n\tMessage      *string         `json:\"message,omitempty\"`\n\tError        *string         `json:\"error,omitempty\"`\n\tErrorMessage *string         `json:\"errorMessage,omitempty\"`\n\tRequestId    *string         `json:\"requestId,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetStatusCode() string {\n\tif r.StatusCode == nil {\n\t\treturn \"\"\n\t}\n\n\tdecoder := json.NewDecoder(bytes.NewReader(r.StatusCode))\n\ttoken, err := decoder.Token()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tswitch t := token.(type) {\n\tcase string:\n\t\treturn t\n\tcase float64:\n\t\treturn strconv.FormatFloat(t, 'f', -1, 64)\n\tcase json.Number:\n\t\treturn t.String()\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nfunc (r *sdkResponseBase) GetError() string {\n\tif r.Error == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Error\n}\n\nfunc (r *sdkResponseBase) GetErrorMessage() string {\n\tif r.ErrorMessage == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.ErrorMessage\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype DomainRecord struct {\n\tDomain      string `json:\"domain\"`\n\tCname       string `json:\"cname\"`\n\tProductCode string `json:\"product_code\"`\n\tProductName string `json:\"product_name\"`\n\tAreaScope   int32  `json:\"area_scope\"`\n\tStatus      int32  `json:\"status\"`\n}\n\ntype DomainDetail struct {\n\tDomainRecord\n\tHttpsStatus string                  `json:\"https_status\"`\n\tHttpsBasic  *DomainHttpsBasicConfig `json:\"https_basic,omitempty\"`\n\tCertName    string                  `json:\"cert_name\"`\n\tSsl         string                  `json:\"ssl\"`\n\tSslStapling string                  `json:\"ssl_stapling\"`\n}\n\ntype DomainHttpsBasicConfig struct {\n\tHttpsForce     string `json:\"https_force\"`\n\tHttpForce      string `json:\"http_force\"`\n\tForceStatus    string `json:\"force_status\"`\n\tOriginProtocol string `json:\"origin_protocol\"`\n}\n\ntype CertRecord struct {\n\tId          int64    `json:\"id\"`\n\tName        string   `json:\"name\"`\n\tCN          string   `json:\"cn\"`\n\tSANs        []string `json:\"sans\"`\n\tUsageMode   int32    `json:\"usage_mode\"`\n\tState       int32    `json:\"state\"`\n\tExpiresTime int64    `json:\"expires\"`\n\tIssueTime   int64    `json:\"issue\"`\n\tIssuer      string   `json:\"issuer\"`\n\tCreatedTime int64    `json:\"created\"`\n}\n\ntype CertDetail struct {\n\tCertRecord\n\tCerts string `json:\"certs\"`\n\tKey   string `json:\"key\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/lvdn/api_create_cert.go",
    "content": "package lvdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype CreateCertRequest struct {\n\tName  *string `json:\"name,omitempty\"`\n\tCerts *string `json:\"certs,omitempty\"`\n\tKey   *string `json:\"key,omitempty\"`\n}\n\ntype CreateCertResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tId int64 `json:\"id\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) {\n\treturn c.CreateCertWithContext(context.Background(), req)\n}\n\nfunc (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/cert/creat-cert\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &CreateCertResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/lvdn/api_query_cert_detail.go",
    "content": "package lvdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype QueryCertDetailRequest struct {\n\tId        *int64  `json:\"id,omitempty\"         url:\"id,omitempty\"`\n\tName      *string `json:\"name,omitempty\"       url:\"name,omitempty\"`\n\tUsageMode *int32  `json:\"usage_mode,omitempty\" url:\"usage_mode,omitempty\"`\n}\n\ntype QueryCertDetailResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tResult *CertDetail `json:\"result,omitempty\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) QueryCertDetail(req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) {\n\treturn c.QueryCertDetailWithContext(context.Background(), req)\n}\n\nfunc (c *Client) QueryCertDetailWithContext(ctx context.Context, req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/cert/query-cert-detail\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &QueryCertDetailResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/lvdn/api_query_cert_list.go",
    "content": "package lvdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype QueryCertListRequest struct {\n\tPage      *int32 `json:\"page,omitempty\"       url:\"page,omitempty\"`\n\tPerPage   *int32 `json:\"per_page,omitempty\"   url:\"per_page,omitempty\"`\n\tUsageMode *int32 `json:\"usage_mode,omitempty\" url:\"usage_mode,omitempty\"`\n}\n\ntype QueryCertListResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tResults      []*CertRecord `json:\"result,omitempty\"`\n\t\tPage         int32         `json:\"page,omitempty\"`\n\t\tPerPage      int32         `json:\"per_page,omitempty\"`\n\t\tTotalPage    int32         `json:\"total_page,omitempty\"`\n\t\tTotalRecords int32         `json:\"total_records,omitempty\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) QueryCertList(req *QueryCertListRequest) (*QueryCertListResponse, error) {\n\treturn c.QueryCertListWithContext(context.Background(), req)\n}\n\nfunc (c *Client) QueryCertListWithContext(ctx context.Context, req *QueryCertListRequest) (*QueryCertListResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/cert/query-cert-list\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &QueryCertListResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/lvdn/api_query_domain_detail.go",
    "content": "package lvdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype QueryDomainDetailRequest struct {\n\tDomain      *string `json:\"domain,omitempty\"       url:\"domain,omitempty\"`\n\tProductCode *string `json:\"product_code,omitempty\" url:\"product_code,omitempty\"`\n}\n\ntype QueryDomainDetailResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *DomainDetail `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) QueryDomainDetail(req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) {\n\treturn c.QueryDomainDetailWithContext(context.Background(), req)\n}\n\nfunc (c *Client) QueryDomainDetailWithContext(ctx context.Context, req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/live/domain/query-domain-detail\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &QueryDomainDetailResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/lvdn/api_query_domain_list.go",
    "content": "package lvdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype QueryDomainListRequest struct {\n\tPage        *int32  `json:\"page,omitempty\"         url:\"page,omitempty\"`\n\tPageSize    *int32  `json:\"page_size,omitempty\"    url:\"page_size,omitempty\"`\n\tDomain      *string `json:\"domain,omitempty\"       url:\"domain,omitempty\"`\n\tProductCode *string `json:\"product_code,omitempty\" url:\"product_code,omitempty\"`\n\tStatus      *int32  `json:\"status,omitempty\"       url:\"status,omitempty\"`\n\tAreaScope   *int32  `json:\"area_scope,omitempty\"   url:\"area_scope,omitempty\"`\n}\n\ntype QueryDomainListResponse struct {\n\tsdkResponseBase\n\n\tReturnObj *struct {\n\t\tResults   []*DomainRecord `json:\"result,omitempty\"`\n\t\tPage      int32           `json:\"page,omitempty\"`\n\t\tPageSize  int32           `json:\"page_size,omitempty\"`\n\t\tPageCount int32           `json:\"page_count,omitempty\"`\n\t\tTotal     int32           `json:\"total,omitempty\"`\n\t} `json:\"returnObj,omitempty\"`\n}\n\nfunc (c *Client) QueryDomainList(req *QueryDomainListRequest) (*QueryDomainListResponse, error) {\n\treturn c.QueryDomainListWithContext(context.Background(), req)\n}\n\nfunc (c *Client) QueryDomainListWithContext(ctx context.Context, req *QueryDomainListRequest) (*QueryDomainListResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/v1/domain/query-domain-list\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &QueryDomainListResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/lvdn/api_update_domain.go",
    "content": "package lvdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype UpdateDomainRequest struct {\n\tDomain      *string `json:\"domain,omitempty\"`\n\tProductCode *string `json:\"product_code,omitempty\"`\n\tHttpsSwitch *int32  `json:\"https_switch,omitempty\"`\n\tCertName    *string `json:\"cert_name,omitempty\"`\n}\n\ntype UpdateDomainResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) UpdateDomain(req *UpdateDomainRequest) (*UpdateDomainResponse, error) {\n\treturn c.UpdateDomainWithContext(context.Background(), req)\n}\n\nfunc (c *Client) UpdateDomainWithContext(ctx context.Context, req *UpdateDomainRequest) (*UpdateDomainResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/live/domain/update-domain\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateDomainResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/lvdn/client.go",
    "content": "package lvdn\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/openapi\"\n\t\"github.com/go-resty/resty/v2\"\n)\n\nconst endpoint = \"https://ctlvdn-global.ctapi.ctyun.cn\"\n\ntype Client struct {\n\tclient *openapi.Client\n}\n\nfunc NewClient(accessKeyId, secretAccessKey string) (*Client, error) {\n\tclient, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{client: client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\treturn c.client.NewRequest(method, path)\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\treturn c.client.DoRequest(req)\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tresp, err := c.client.DoRequestWithResult(req, res)\n\tif err == nil {\n\t\tif tcode := res.GetStatusCode(); tcode != \"\" && tcode != \"100000\" {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: code='%s', message='%s', errorCode='%s', errorMessage='%s'\", tcode, res.GetMessage(), res.GetMessage(), res.GetErrorMessage())\n\t\t}\n\t}\n\n\treturn resp, err\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/lvdn/types.go",
    "content": "package lvdn\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"strconv\"\n)\n\ntype sdkResponse interface {\n\tGetStatusCode() string\n\tGetMessage() string\n\tGetError() string\n\tGetErrorMessage() string\n}\n\ntype sdkResponseBase struct {\n\tStatusCode   json.RawMessage `json:\"statusCode,omitempty\"`\n\tMessage      *string         `json:\"message,omitempty\"`\n\tError        *string         `json:\"error,omitempty\"`\n\tErrorMessage *string         `json:\"errorMessage,omitempty\"`\n\tRequestId    *string         `json:\"requestId,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetStatusCode() string {\n\tif r.StatusCode == nil {\n\t\treturn \"\"\n\t}\n\n\tdecoder := json.NewDecoder(bytes.NewReader(r.StatusCode))\n\ttoken, err := decoder.Token()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tswitch t := token.(type) {\n\tcase string:\n\t\treturn t\n\tcase float64:\n\t\treturn strconv.FormatFloat(t, 'f', -1, 64)\n\tcase json.Number:\n\t\treturn t.String()\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nfunc (r *sdkResponseBase) GetError() string {\n\tif r.Error == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Error\n}\n\nfunc (r *sdkResponseBase) GetErrorMessage() string {\n\tif r.ErrorMessage == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.ErrorMessage\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype DomainRecord struct {\n\tDomain      string `json:\"domain\"`\n\tCname       string `json:\"cname\"`\n\tProductCode string `json:\"product_code\"`\n\tProductName string `json:\"product_name\"`\n\tAreaScope   int32  `json:\"area_scope\"`\n\tStatus      int32  `json:\"status\"`\n}\n\ntype DomainDetail struct {\n\tDomainRecord\n\tHttpsSwitch int32  `json:\"https_switch\"`\n\tCertName    string `json:\"cert_name\"`\n}\n\ntype CertRecord struct {\n\tId          int64    `json:\"id\"`\n\tName        string   `json:\"name\"`\n\tCN          string   `json:\"cn\"`\n\tSANs        []string `json:\"sans\"`\n\tUsageMode   int32    `json:\"usage_mode\"`\n\tState       int32    `json:\"state\"`\n\tExpiresTime int64    `json:\"expires\"`\n\tIssueTime   int64    `json:\"issue\"`\n\tIssuer      string   `json:\"issuer\"`\n\tCreatedTime int64    `json:\"created\"`\n}\n\ntype CertDetail struct {\n\tCertRecord\n\tCerts string `json:\"certs\"`\n\tKey   string `json:\"key\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ctyun/openapi/client.go",
    "content": "package openapi\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/pocketbase/pocketbase/tools/security\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(endpoint, accessKeyId, secretAccessKey string) (*Client, error) {\n\tif endpoint == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset endpoint\")\n\t}\n\tif _, err := url.Parse(endpoint); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid endpoint: %w\", err)\n\t}\n\tif accessKeyId == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset accessKeyId\")\n\t}\n\tif secretAccessKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset secretAccessKey\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(endpoint).\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\t// 生成时间戳及流水号\n\t\t\tnow := time.Now()\n\t\t\teopDate := now.Format(\"20060102T150405Z\")\n\t\t\teopReqId := security.RandomString(32)\n\n\t\t\t// 获取查询参数\n\t\t\tqueryStr := \"\"\n\t\t\tif req.URL != nil {\n\t\t\t\tqueryStr = req.URL.Query().Encode()\n\t\t\t}\n\n\t\t\t// 获取请求正文\n\t\t\tpayloadStr := \"\"\n\t\t\tif req.Body != nil {\n\t\t\t\treader, err := req.GetBody()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tdefer reader.Close()\n\t\t\t\tpayload, err := io.ReadAll(reader)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tpayloadStr = string(payload)\n\t\t\t}\n\n\t\t\t// 构造代签字符串\n\t\t\tpayloadHash := sha256.Sum256([]byte(payloadStr))\n\t\t\tpayloadHashHex := hex.EncodeToString(payloadHash[:])\n\t\t\tdataToSign := fmt.Sprintf(\"ctyun-eop-request-id:%s\\neop-date:%s\\n\\n%s\\n%s\", eopReqId, eopDate, queryStr, payloadHashHex)\n\n\t\t\t// 生成 ktime\n\t\t\thasher := hmac.New(sha256.New, []byte(secretAccessKey))\n\t\t\thasher.Write([]byte(eopDate))\n\t\t\tktime := hasher.Sum(nil)\n\n\t\t\t// 生成 kak\n\t\t\thasher = hmac.New(sha256.New, ktime)\n\t\t\thasher.Write([]byte(accessKeyId))\n\t\t\tkak := hasher.Sum(nil)\n\n\t\t\t// 生成 kdate\n\t\t\thasher = hmac.New(sha256.New, kak)\n\t\t\thasher.Write([]byte(now.Format(\"20060102\")))\n\t\t\tkdate := hasher.Sum(nil)\n\n\t\t\t// 构造签名\n\t\t\thasher = hmac.New(sha256.New, kdate)\n\t\t\thasher.Write([]byte(dataToSign))\n\t\t\tsign := hasher.Sum(nil)\n\t\t\tsignStr := base64.StdEncoding.EncodeToString(sign)\n\n\t\t\t// 设置请求头\n\t\t\treq.Header.Set(\"ctyun-eop-request-id\", eopReqId)\n\t\t\treq.Header.Set(\"eop-date\", eopDate)\n\t\t\treq.Header.Set(\"eop-authorization\", fmt.Sprintf(\"%s Headers=ctyun-eop-request-id;eop-date Signature=%s\", accessKeyId, signStr))\n\n\t\t\treturn nil\n\t\t})\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) NewRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) DoRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) DoRequestWithResult(req *resty.Request, res interface{}) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.DoRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dcloud/unicloud/api_create_domain_with_cert.go",
    "content": "package unicloud\n\nimport (\n\t\"net/http\"\n)\n\ntype CreateDomainWithCertRequest struct {\n\tProvider string `json:\"provider\"`\n\tSpaceId  string `json:\"spaceId\"`\n\tDomain   string `json:\"domain\"`\n\tCert     string `json:\"cert\"`\n\tKey      string `json:\"key\"`\n}\n\ntype CreateDomainWithCertResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) CreateDomainWithCert(req *CreateDomainWithCertRequest) (*CreateDomainWithCertResponse, error) {\n\tif err := c.ensureApiUserTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp := &CreateDomainWithCertResponse{}\n\terr := c.sendRequestWithResult(http.MethodPost, \"/host/create-domain-with-cert\", req, resp)\n\treturn resp, err\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dcloud/unicloud/client.go",
    "content": "package unicloud\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tusername string\n\tpassword string\n\n\tserverlessJwtToken    string\n\tserverlessJwtTokenExp time.Time\n\tserverlessJwtTokenMtx sync.Mutex\n\n\tserverlessClient *resty.Client\n\n\tapiUserToken    string\n\tapiUserTokenMtx sync.Mutex\n\n\tapiClient *resty.Client\n}\n\nconst (\n\tuniIdentityEndpoint     = \"https://account.dcloud.net.cn/client\"\n\tuniIdentityClientSecret = \"ba461799-fde8-429f-8cc4-4b6d306e2339\"\n\tuniIdentityAppId        = \"__UNI__uniid_server\"\n\tuniIdentitySpaceId      = \"uni-id-server\"\n\tuniConsoleEndpoint      = \"https://unicloud.dcloud.net.cn/client\"\n\tuniConsoleClientSecret  = \"4c1f7fbf-c732-42b0-ab10-4634a8bbe834\"\n\tuniConsoleAppId         = \"__UNI__unicloud_console\"\n\tuniConsoleSpaceId       = \"dc-6nfabcn6ada8d3dd\"\n)\n\nfunc NewClient(username, password string) (*Client, error) {\n\tif username == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset username\")\n\t}\n\tif password == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset password\")\n\t}\n\n\tclient := &Client{\n\t\tusername: username,\n\t\tpassword: password,\n\t}\n\tclient.serverlessClient = resty.New().\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent)\n\tclient.apiClient = resty.New().\n\t\tSetBaseURL(\"https://unicloud-api.dcloud.net.cn/unicloud/api\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\tif client.apiUserToken != \"\" {\n\t\t\t\treq.Header.Set(\"Token\", client.apiUserToken)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\treturn client, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.serverlessClient.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) buildServerlessClientInfo(appId string) (_clientInfo map[string]any, _err error) {\n\treturn map[string]any{\n\t\t\"PLATFORM\":           \"web\",\n\t\t\"OS\":                 strings.ToUpper(runtime.GOOS),\n\t\t\"APPID\":              appId,\n\t\t\"DEVICEID\":           app.AppName,\n\t\t\"LOCALE\":             \"zh-Hans\",\n\t\t\"osName\":             runtime.GOOS,\n\t\t\"appId\":              appId,\n\t\t\"appName\":            \"uniCloud\",\n\t\t\"deviceId\":           app.AppName,\n\t\t\"deviceType\":         \"pc\",\n\t\t\"uniPlatform\":        \"web\",\n\t\t\"uniCompilerVersion\": \"4.45\",\n\t\t\"uniRuntimeVersion\":  \"4.45\",\n\t}, nil\n}\n\nfunc (c *Client) buildServerlessPayloadInfo(appId, spaceId, target, method, action string, params, data interface{}) (map[string]any, error) {\n\tclientInfo, err := c.buildServerlessClientInfo(appId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfunctionArgsParams := make([]any, 0)\n\tif params != nil {\n\t\tfunctionArgsParams = append(functionArgsParams, params)\n\t}\n\n\tfunctionArgs := map[string]any{\n\t\t\"clientInfo\": clientInfo,\n\t\t\"uniIdToken\": c.serverlessJwtToken,\n\t}\n\tif method != \"\" {\n\t\tfunctionArgs[\"method\"] = method\n\t\tfunctionArgs[\"params\"] = make([]any, 0)\n\t}\n\tif action != \"\" {\n\t\ttype _obj struct{}\n\t\tfunctionArgs[\"action\"] = action\n\t\tfunctionArgs[\"data\"] = &_obj{}\n\t}\n\tif params != nil {\n\t\tfunctionArgs[\"params\"] = []any{params}\n\t}\n\tif data != nil {\n\t\tfunctionArgs[\"data\"] = data\n\t}\n\n\tjsonb, err := json.Marshal(map[string]any{\n\t\t\"functionTarget\": target,\n\t\t\"functionArgs\":   functionArgs,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpayload := map[string]any{\n\t\t\"method\":    \"serverless.function.runtime.invoke\",\n\t\t\"params\":    string(jsonb),\n\t\t\"spaceId\":   spaceId,\n\t\t\"timestamp\": time.Now().UnixMilli(),\n\t}\n\n\treturn payload, nil\n}\n\nfunc (c *Client) invokeServerless(endpoint, clientSecret, appId, spaceId, target, method, action string, params, data interface{}) (*resty.Response, error) {\n\tif endpoint == \"\" {\n\t\treturn nil, fmt.Errorf(\"unicloud api error: endpoint cannot be empty\")\n\t}\n\n\tpayload, err := c.buildServerlessPayloadInfo(appId, spaceId, target, method, action, params, data)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unicloud api error: failed to build request: %w\", err)\n\t}\n\n\tclientInfo, _ := c.buildServerlessClientInfo(appId)\n\tclientInfoJsonb, _ := json.Marshal(clientInfo)\n\n\tsign := generateSignature(payload, clientSecret)\n\n\treq := c.serverlessClient.R().\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"Origin\", \"https://unicloud.dcloud.net.cn\").\n\t\tSetHeader(\"Referer\", \"https://unicloud.dcloud.net.cn\").\n\t\tSetHeader(\"X-Client-Info\", string(clientInfoJsonb)).\n\t\tSetHeader(\"X-Client-Token\", c.serverlessJwtToken).\n\t\tSetHeader(\"X-Serverless-Sign\", sign).\n\t\tSetBody(payload)\n\tresp, err := req.Post(endpoint)\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"unicloud api error: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"unicloud api error: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) invokeServerlessWithResult(endpoint, clientSecret, appId, spaceId, target, method, action string, params, data interface{}, result sdkResponse) error {\n\tresp, err := c.invokeServerless(endpoint, clientSecret, appId, spaceId, target, method, action, params, data)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &result)\n\t\t}\n\t\treturn err\n\t}\n\n\tif err := json.Unmarshal(resp.Body(), &result); err != nil {\n\t\treturn fmt.Errorf(\"unicloud api error: failed to unmarshal response: %w\", err)\n\t} else if success := result.GetSuccess(); !success {\n\t\treturn fmt.Errorf(\"unicloud api error: code='%s', message='%s'\", result.GetErrorCode(), result.GetErrorMessage())\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) sendRequest(method string, path string, params interface{}) (*resty.Response, error) {\n\treq := c.apiClient.R()\n\tif strings.EqualFold(method, http.MethodGet) {\n\t\tqs := make(map[string]string)\n\t\tif params != nil {\n\t\t\ttemp := make(map[string]any)\n\t\t\tjsonb, _ := json.Marshal(params)\n\t\t\tjson.Unmarshal(jsonb, &temp)\n\t\t\tfor k, v := range temp {\n\t\t\t\tif v != nil {\n\t\t\t\t\tqs[k] = fmt.Sprintf(\"%v\", v)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treq = req.SetQueryParams(qs)\n\t} else {\n\t\treq = req.SetHeader(\"Content-Type\", \"application/json\").SetBody(params)\n\t}\n\n\tresp, err := req.Execute(method, path)\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"unicloud api error: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"unicloud api error: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) sendRequestWithResult(method string, path string, params interface{}, result sdkResponse) error {\n\tresp, err := c.sendRequest(method, path, params)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &result)\n\t\t}\n\t\treturn err\n\t}\n\n\tif err := json.Unmarshal(resp.Body(), &result); err != nil {\n\t\treturn fmt.Errorf(\"unicloud api error: failed to unmarshal response: %w\", err)\n\t} else if retcode := result.GetReturnCode(); retcode != 0 {\n\t\treturn fmt.Errorf(\"unicloud api error: ret='%d', desc='%s'\", retcode, result.GetReturnDesc())\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) ensureServerlessJwtTokenExists() error {\n\tc.serverlessJwtTokenMtx.Lock()\n\tdefer c.serverlessJwtTokenMtx.Unlock()\n\tif c.serverlessJwtToken != \"\" && c.serverlessJwtTokenExp.After(time.Now()) {\n\t\treturn nil\n\t}\n\n\tparams := map[string]string{\n\t\t\"password\": \"password\",\n\t}\n\tif regexp.MustCompile(`^1\\d{10}$`).MatchString(c.username) {\n\t\tparams[\"mobile\"] = c.username\n\t} else if regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$`).MatchString(c.username) {\n\t\tparams[\"email\"] = c.username\n\t} else {\n\t\tparams[\"username\"] = c.username\n\t}\n\n\ttype loginResponse struct {\n\t\tsdkResponseBase\n\t\tData *struct {\n\t\t\tCode     int32  `json:\"errCode\"`\n\t\t\tUID      string `json:\"uid\"`\n\t\t\tNewToken *struct {\n\t\t\t\tToken        string `json:\"token\"`\n\t\t\t\tTokenExpired int64  `json:\"tokenExpired\"`\n\t\t\t} `json:\"newToken,omitempty\"`\n\t\t} `json:\"data,omitempty\"`\n\t}\n\n\tresp := &loginResponse{}\n\tif err := c.invokeServerlessWithResult(\n\t\tuniIdentityEndpoint, uniIdentityClientSecret, uniIdentityAppId, uniIdentitySpaceId,\n\t\t\"uni-id-co\", \"login\", \"\", params, nil,\n\t\tresp); err != nil {\n\t\treturn err\n\t} else if resp.Data == nil || resp.Data.NewToken == nil || resp.Data.NewToken.Token == \"\" {\n\t\treturn fmt.Errorf(\"unicloud api error: received empty token\")\n\t}\n\n\tc.serverlessJwtToken = resp.Data.NewToken.Token\n\tc.serverlessJwtTokenExp = time.UnixMilli(resp.Data.NewToken.TokenExpired)\n\n\treturn nil\n}\n\nfunc (c *Client) ensureApiUserTokenExists() error {\n\tif err := c.ensureServerlessJwtTokenExists(); err != nil {\n\t\treturn err\n\t}\n\n\tc.apiUserTokenMtx.Lock()\n\tdefer c.apiUserTokenMtx.Unlock()\n\tif c.apiUserToken != \"\" {\n\t\treturn nil\n\t}\n\n\ttype getUserTokenResponse struct {\n\t\tsdkResponseBase\n\t\tData *struct {\n\t\t\tCode int32 `json:\"code\"`\n\t\t\tData *struct {\n\t\t\t\tResult      int32  `json:\"ret\"`\n\t\t\t\tDescription string `json:\"desc\"`\n\t\t\t\tData        *struct {\n\t\t\t\t\tEmail string `json:\"email\"`\n\t\t\t\t\tToken string `json:\"token\"`\n\t\t\t\t} `json:\"data,omitempty\"`\n\t\t\t} `json:\"data,omitempty\"`\n\t\t} `json:\"data,omitempty\"`\n\t}\n\n\tresp := &getUserTokenResponse{}\n\tif err := c.invokeServerlessWithResult(\n\t\tuniConsoleEndpoint, uniConsoleClientSecret, uniConsoleAppId, uniConsoleSpaceId,\n\t\t\"uni-cloud-kernel\", \"\", \"user/getUserToken\", nil, map[string]any{\"isLogin\": true},\n\t\tresp); err != nil {\n\t\treturn err\n\t} else if resp.Data == nil || resp.Data.Data == nil || resp.Data.Data.Data == nil || resp.Data.Data.Data.Token == \"\" {\n\t\treturn fmt.Errorf(\"unicloud api error: received empty user token\")\n\t}\n\n\tc.apiUserToken = resp.Data.Data.Data.Token\n\n\treturn nil\n}\n\nfunc generateSignature(params map[string]any, secret string) string {\n\tkeys := make([]string, 0, len(params))\n\tfor k := range params {\n\t\tkeys = append(keys, k)\n\t}\n\tsort.Strings(keys)\n\n\tcanonicalStr := \"\"\n\tfor i, k := range keys {\n\t\tif i > 0 {\n\t\t\tcanonicalStr += \"&\"\n\t\t}\n\t\tcanonicalStr += k + \"=\" + fmt.Sprintf(\"%v\", params[k])\n\t}\n\n\tmac := hmac.New(md5.New, []byte(secret))\n\tmac.Write([]byte(canonicalStr))\n\tsign := mac.Sum(nil)\n\tsignHex := hex.EncodeToString(sign)\n\n\treturn signHex\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dcloud/unicloud/types.go",
    "content": "package unicloud\n\ntype sdkResponse interface {\n\tGetSuccess() bool\n\tGetErrorCode() string\n\tGetErrorMessage() string\n\tGetReturnCode() int\n\tGetReturnDesc() string\n}\n\ntype sdkResponseBase struct {\n\tSuccess *bool              `json:\"success,omitempty\"`\n\tHeader  *map[string]string `json:\"header,omitempty\"`\n\tError   *struct {\n\t\tCode    string `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error,omitempty\"`\n\tReturnCode *int    `json:\"ret,omitempty\"`\n\tReturnDesc *string `json:\"desc,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetReturnCode() int {\n\tif r.ReturnCode == nil {\n\t\treturn 0\n\t}\n\n\treturn *r.ReturnCode\n}\n\nfunc (r *sdkResponseBase) GetReturnDesc() string {\n\tif r.ReturnDesc == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.ReturnDesc\n}\n\nfunc (r *sdkResponseBase) GetSuccess() bool {\n\tif r.Success == nil {\n\t\treturn false\n\t}\n\n\treturn *r.Success\n}\n\nfunc (r *sdkResponseBase) GetErrorCode() string {\n\tif r.Error == nil {\n\t\treturn \"\"\n\t}\n\n\treturn r.Error.Code\n}\n\nfunc (r *sdkResponseBase) GetErrorMessage() string {\n\tif r.Error == nil {\n\t\treturn \"\"\n\t}\n\n\treturn r.Error.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n"
  },
  {
    "path": "pkg/sdk3rd/dnsla/api_create_record.go",
    "content": "package dnsla\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype CreateRecordRequest struct {\n\tDomainId   *string `json:\"domainId\"`\n\tGroupId    *string `json:\"groupId,omitempty\"`\n\tLineId     *string `json:\"lineId,omitempty\"`\n\tType       *int32  `json:\"type\"`\n\tHost       *string `json:\"host\"`\n\tData       *string `json:\"data\"`\n\tTtl        *int32  `json:\"ttl\"`\n\tWeight     *int32  `json:\"weight,omitempty\"`\n\tPreference *int32  `json:\"preference,omitempty\"`\n}\n\ntype CreateRecordResponse struct {\n\tsdkResponseBase\n\tData *struct {\n\t\tId string `json:\"id\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) CreateRecord(req *CreateRecordRequest) (*CreateRecordResponse, error) {\n\treturn c.CreateRecordWithContext(context.Background(), req)\n}\n\nfunc (c *Client) CreateRecordWithContext(ctx context.Context, req *CreateRecordRequest) (*CreateRecordResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/record\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &CreateRecordResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dnsla/api_delete_record.go",
    "content": "package dnsla\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype DeleteRecordResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) DeleteRecord(recordId string) (*DeleteRecordResponse, error) {\n\treturn c.DeleteRecordWithContext(context.Background(), recordId)\n}\n\nfunc (c *Client) DeleteRecordWithContext(ctx context.Context, recordId string) (*DeleteRecordResponse, error) {\n\tif recordId == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset recordId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodDelete, \"/record\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetQueryParam(\"id\", recordId)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &DeleteRecordResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dnsla/api_list_domains.go",
    "content": "package dnsla\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype ListDomainsRequest struct {\n\tGroupId   *string `json:\"groupId,omitempty\"   url:\"groupId,omitempty\"`\n\tPageIndex *int32  `json:\"pageIndex,omitempty\" url:\"pageIndex,omitempty\"`\n\tPageSize  *int32  `json:\"pageSize,omitempty\"  url:\"pageSize,omitempty\"`\n}\n\ntype ListDomainsResponse struct {\n\tsdkResponseBase\n\tData *struct {\n\t\tTotal   int32           `json:\"total\"`\n\t\tResults []*DomainRecord `json:\"results\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) ListDomains(req *ListDomainsRequest) (*ListDomainsResponse, error) {\n\treturn c.ListDomainsWithContext(context.Background(), req)\n}\n\nfunc (c *Client) ListDomainsWithContext(ctx context.Context, req *ListDomainsRequest) (*ListDomainsResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/domainList\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ListDomainsResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dnsla/api_list_records.go",
    "content": "package dnsla\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype ListRecordsRequest struct {\n\tDomainId  *string `json:\"domainId,omitempty\"  url:\"domainId,omitempty\"`\n\tGroupId   *string `json:\"groupId,omitempty\"   url:\"groupId,omitempty\"`\n\tLineId    *string `json:\"lineId,omitempty\"    url:\"lineId,omitempty\"`\n\tType      *int32  `json:\"type,omitempty\"      url:\"type,omitempty\"`\n\tHost      *string `json:\"host,omitempty\"      url:\"host,omitempty\"`\n\tData      *string `json:\"data,omitempty\"      url:\"data,omitempty\"`\n\tPageIndex *int32  `json:\"pageIndex,omitempty\" url:\"pageIndex,omitempty\"`\n\tPageSize  *int32  `json:\"pageSize,omitempty\"  url:\"pageSize,omitempty\"`\n}\n\ntype ListRecordsResponse struct {\n\tsdkResponseBase\n\tData *struct {\n\t\tTotal   int32        `json:\"total\"`\n\t\tResults []*DnsRecord `json:\"results\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) ListRecords(req *ListRecordsRequest) (*ListRecordsResponse, error) {\n\treturn c.ListRecordsWithContext(context.Background(), req)\n}\n\nfunc (c *Client) ListRecordsWithContext(ctx context.Context, req *ListRecordsRequest) (*ListRecordsResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/recordList\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ListRecordsResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dnsla/api_update_record.go",
    "content": "package dnsla\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype UpdateRecordRequest struct {\n\tId         *string `json:\"id\"`\n\tGroupId    *string `json:\"groupId,omitempty\"`\n\tLineId     *string `json:\"lineId,omitempty\"`\n\tType       *int32  `json:\"type,omitempty\"`\n\tHost       *string `json:\"host,omitempty\"`\n\tData       *string `json:\"data,omitempty\"`\n\tTtl        *int32  `json:\"ttl,omitempty\"`\n\tWeight     *int32  `json:\"weight,omitempty\"`\n\tPreference *int32  `json:\"preference,omitempty\"`\n}\n\ntype UpdateRecordResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) UpdateRecord(req *UpdateRecordRequest) (*UpdateRecordResponse, error) {\n\treturn c.UpdateRecordWithContext(context.Background(), req)\n}\n\nfunc (c *Client) UpdateRecordWithContext(ctx context.Context, req *UpdateRecordRequest) (*UpdateRecordResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPut, \"/record\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateRecordResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dnsla/client.go",
    "content": "package dnsla\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(apiId, apiSecret string) (*Client, error) {\n\tif apiId == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiId\")\n\t}\n\tif apiSecret == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiSecret\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(\"https://api.dns.la/api\").\n\t\tSetBasicAuth(apiId, apiSecret).\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent)\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tcode := res.GetCode(); tcode/100 != 2 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: code='%d', message='%s'\", tcode, res.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dnsla/types.go",
    "content": "package dnsla\n\ntype sdkResponse interface {\n\tGetCode() int\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    *int    `json:\"code,omitempty\"`\n\tMessage *string `json:\"message,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() int {\n\tif r.Code == nil {\n\t\treturn 0\n\t}\n\n\treturn *r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype DomainRecord struct {\n\tId            string `json:\"id\"`\n\tGroupId       string `json:\"groupId\"`\n\tGroupName     string `json:\"groupName\"`\n\tDomain        string `json:\"domain\"`\n\tDisplayDomain string `json:\"displayDomain\"`\n\tCreatedAt     int64  `json:\"createdAt\"`\n\tUpdatedAt     int64  `json:\"updatedAt\"`\n}\n\ntype DnsRecord struct {\n\tId          string `json:\"id\"`\n\tDomainId    string `json:\"domainId\"`\n\tGroupId     string `json:\"groupId\"`\n\tGroupName   string `json:\"groupName\"`\n\tLineId      string `json:\"lineId\"`\n\tLineCode    string `json:\"lineCode\"`\n\tLineName    string `json:\"lineName\"`\n\tType        int32  `json:\"type\"`\n\tHost        string `json:\"host\"`\n\tDisplayHost string `json:\"displayHost\"`\n\tData        string `json:\"data\"`\n\tDisplayData string `json:\"displayData\"`\n\tTtl         int32  `json:\"ttl\"`\n\tWeight      int32  `json:\"weight\"`\n\tPreference  int32  `json:\"preference\"`\n\tCreatedAt   int64  `json:\"createdAt\"`\n\tUpdatedAt   int64  `json:\"updatedAt\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dogecloud/api_bind_cdn_cert.go",
    "content": "package dogecloud\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype BindCdnCertRequest struct {\n\tCertId int64  `json:\"id\"`\n\tDomain string `json:\"domain\"`\n}\n\ntype BindCdnCertResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) BindCdnCert(req *BindCdnCertRequest) (*BindCdnCertResponse, error) {\n\treturn c.BindCdnCertWithContext(context.Background(), req)\n}\n\nfunc (c *Client) BindCdnCertWithContext(ctx context.Context, req *BindCdnCertRequest) (*BindCdnCertResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/cdn/cert/bind.json\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &BindCdnCertResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dogecloud/api_list_cdn_domain.go",
    "content": "package dogecloud\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n)\n\ntype ListCdnDomainResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tDomains []*struct {\n\t\t\tId          int64           `json:\"id\"`\n\t\t\tName        string          `json:\"name\"`\n\t\t\tCname       string          `json:\"cname\"`\n\t\t\tServiceType string          `json:\"service_type\"`\n\t\t\tStatus      string          `json:\"status\"`\n\t\t\tSource      json.RawMessage `json:\"source\"`\n\t\t\tCreateTime  string          `json:\"ctime\"`\n\t\t\tCertId      int64           `json:\"cert_id\"`\n\t\t} `json:\"domains\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) ListCdnDomain() (*ListCdnDomainResponse, error) {\n\treturn c.ListCdnDomainWithContext(context.Background())\n}\n\nfunc (c *Client) ListCdnDomainWithContext(ctx context.Context) (*ListCdnDomainResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/cdn/domain/list.json\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ListCdnDomainResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dogecloud/api_upload_cdn_cert.go",
    "content": "package dogecloud\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype UploadCdnCertRequest struct {\n\tNote        string `json:\"note\"`\n\tCertificate string `json:\"cert\"`\n\tPrivateKey  string `json:\"private\"`\n}\n\ntype UploadCdnCertResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tId int64 `json:\"id\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) UploadCdnCert(req *UploadCdnCertRequest) (*UploadCdnCertResponse, error) {\n\treturn c.UploadCdnCertWithContext(context.Background(), req)\n}\n\nfunc (c *Client) UploadCdnCertWithContext(ctx context.Context, req *UploadCdnCertRequest) (*UploadCdnCertResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/cdn/cert/upload.json\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UploadCdnCertResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dogecloud/client.go",
    "content": "package dogecloud\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(accessKey, secretKey string) (*Client, error) {\n\tif accessKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset accessKey\")\n\t}\n\tif secretKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset secretKey\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(\"https://api.dogecloud.com\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(ctx *resty.Client, req *http.Request) error {\n\t\t\trequestUrl := req.URL.Path\n\t\t\trequestQuery := req.URL.Query().Encode()\n\t\t\tif requestQuery != \"\" {\n\t\t\t\trequestUrl += \"?\" + requestQuery\n\t\t\t}\n\n\t\t\tpayload := \"\"\n\t\t\tif req.Body != nil {\n\t\t\t\treader, err := req.GetBody()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tdefer reader.Close()\n\n\t\t\t\tpayloadb, err := io.ReadAll(reader)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tpayload = string(payloadb)\n\t\t\t}\n\n\t\t\tstringToSign := fmt.Sprintf(\"%s\\n%s\", requestUrl, payload)\n\t\t\tmac := hmac.New(sha1.New, []byte(secretKey))\n\t\t\tmac.Write([]byte(stringToSign))\n\t\t\tsign := hex.EncodeToString(mac.Sum(nil))\n\n\t\t\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"TOKEN %s:%s\", accessKey, sign))\n\n\t\t\treturn nil\n\t\t})\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tcode := res.GetCode(); tcode != 0 && tcode != 200 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: code='%d', msg='%s'\", tcode, res.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dogecloud/types.go",
    "content": "package dogecloud\n\ntype sdkResponse interface {\n\tGetCode() int\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    *int    `json:\"code,omitempty\"`\n\tMessage *string `json:\"msg,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() int {\n\tif r.Code == nil {\n\t\treturn 0\n\t}\n\n\treturn *r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n"
  },
  {
    "path": "pkg/sdk3rd/dokploy/api_certificates_all.go",
    "content": "package dokploy\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype CertificatesAllRequest struct{}\n\ntype CertificatesAllResponse = []*Certificate\n\nfunc (c *Client) CertificatesAll(req *CertificatesAllRequest) (*CertificatesAllResponse, error) {\n\treturn c.CertificatesAllWithContext(context.Background(), req)\n}\n\nfunc (c *Client) CertificatesAllWithContext(ctx context.Context, req *CertificatesAllRequest) (*CertificatesAllResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/certificates.all\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &CertificatesAllResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dokploy/api_certificates_create.go",
    "content": "package dokploy\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype CertificatesCreateRequest struct {\n\tCertificateId   *string `json:\"certificateId,omitempty\"`\n\tName            *string `json:\"name,omitempty\"`\n\tCertificateData *string `json:\"certificateData,omitempty\"`\n\tPrivateKey      *string `json:\"privateKey,omitempty\"`\n\tOrganizationId  *string `json:\"organizationId,omitempty\"`\n\tServerId        *string `json:\"serverId,omitempty\"`\n}\n\ntype CertificatesCreateResponse = Certificate\n\nfunc (c *Client) CertificatesCreate(req *CertificatesCreateRequest) (*CertificatesCreateResponse, error) {\n\treturn c.CertificatesCreateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) CertificatesCreateWithContext(ctx context.Context, req *CertificatesCreateRequest) (*CertificatesCreateResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/certificates.create\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &CertificatesCreateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dokploy/api_user_get.go",
    "content": "package dokploy\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype UserGetRequest struct{}\n\ntype UserGetResponse struct {\n\tId             string `json:\"id\"`\n\tOrganizationId string `json:\"organizationId\"`\n\tUserId         string `json:\"userId\"`\n\tRole           string `json:\"role\"`\n\tCreatedAt      string `json:\"createdAt\"`\n\tTeamId         string `json:\"teamId,omitempty\"`\n\tIsDefault      bool   `json:\"isDefault\"`\n\tUser           *struct {\n\t\tId            string `json:\"id\"`\n\t\tFirstName     string `json:\"firstName\"`\n\t\tLastName      string `json:\"lastName\"`\n\t\tEmail         string `json:\"email\"`\n\t\tEmailVerified bool   `json:\"emailVerified\"`\n\t\tRole          string `json:\"role\"`\n\t\tCreatedAt     string `json:\"createdAt\"`\n\t} `json:\"user,omitempty\"`\n}\n\nfunc (c *Client) UserGet(req *UserGetRequest) (*UserGetResponse, error) {\n\treturn c.UserGetWithContext(context.Background(), req)\n}\n\nfunc (c *Client) UserGetWithContext(ctx context.Context, req *UserGetRequest) (*UserGetResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/user.get\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UserGetResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dokploy/client.go",
    "content": "package dokploy\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl string, apiKey string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif apiKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiKey\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")+\"/api\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetHeader(\"X-Api-Key\", apiKey)\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res interface{}) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dokploy/types.go",
    "content": "﻿package dokploy\n\ntype Certificate struct {\n\tCertificateId   string `json:\"certificateId\"`\n\tName            string `json:\"name\"`\n\tCertificateData string `json:\"certificateData\"`\n\tPrivateKey      string `json:\"privateKey\"`\n\tCertificatePath string `json:\"certificatePath,omitempty\"`\n\tOrganizationId  string `json:\"organizationId,omitempty\"`\n\tServerId        string `json:\"serverId,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dynv6/api_add_record.go",
    "content": "package dynv6\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype AddRecordRequest struct {\n\tType     *string `json:\"type,omitempty\"`\n\tName     *string `json:\"name,omitempty\"`\n\tPort     *int    `json:\"port,omitempty\"`\n\tWeight   *int    `json:\"weight,omitempty\"`\n\tPriority *int    `json:\"priority,omitempty\"`\n\tData     *string `json:\"data,omitempty\"`\n\tFlags    *int    `json:\"flags,omitempty\"`\n\tTag      *string `json:\"tag,omitempty\"`\n}\n\ntype AddRecordResponse DNSRecord\n\nfunc (c *Client) AddRecord(zoneID int64, req *AddRecordRequest) (*AddRecordResponse, error) {\n\treturn c.AddRecordWithContext(context.Background(), zoneID, req)\n}\n\nfunc (c *Client) AddRecordWithContext(ctx context.Context, zoneID int64, req *AddRecordRequest) (*AddRecordResponse, error) {\n\tif zoneID == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset zoneID\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf(\"/zones/%d/records\", zoneID))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &AddRecordResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dynv6/api_delete_record.go",
    "content": "package dynv6\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype DeleteRecordResponse DNSRecord\n\nfunc (c *Client) DeleteRecord(zoneID int64, recordID int64) (*DeleteRecordResponse, error) {\n\treturn c.DeleteRecordWithContext(context.Background(), zoneID, recordID)\n}\n\nfunc (c *Client) DeleteRecordWithContext(ctx context.Context, zoneID int64, recordID int64) (*DeleteRecordResponse, error) {\n\tif zoneID == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset zoneID\")\n\t}\n\tif recordID == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset recordID\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodDelete, fmt.Sprintf(\"/zones/%d/records/%d\", zoneID, recordID))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &DeleteRecordResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dynv6/api_list_records.go",
    "content": "package dynv6\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype ListRecordsResponse []*DNSRecord\n\nfunc (c *Client) ListRecords(zoneID int64) (*ListRecordsResponse, error) {\n\treturn c.ListRecordsWithContext(context.Background(), zoneID)\n}\n\nfunc (c *Client) ListRecordsWithContext(ctx context.Context, zoneID int64) (*ListRecordsResponse, error) {\n\tif zoneID == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset zoneID\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf(\"/zones/%d/records\", zoneID))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ListRecordsResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dynv6/api_list_zones.go",
    "content": "package dynv6\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype ListZonesResponse []*ZoneRecord\n\nfunc (c *Client) ListZones() (*ListZonesResponse, error) {\n\treturn c.ListZonesWithContext(context.Background())\n}\n\nfunc (c *Client) ListZonesWithContext(ctx context.Context) (*ListZonesResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/zones\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ListZonesResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dynv6/client.go",
    "content": "package dynv6\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(httpToken string) (*Client, error) {\n\tif httpToken == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset httpToken\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(\"https://dynv6.com/api/v2\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Authorization\", \"Bearer \"+httpToken).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent)\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res interface{}) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/dynv6/types.go",
    "content": "package dynv6\n\ntype ZoneRecord struct {\n\tID          int64  `json:\"id\"`\n\tName        string `json:\"name\"`\n\tIPv4Address string `json:\"ipv4address\"`\n\tIPv6Prefix  string `json:\"ipv6prefix\"`\n\tCreatedAt   string `json:\"createdAt\"`\n\tUpdatedAt   string `json:\"updatedAt\"`\n}\n\ntype DNSRecord struct {\n\tID           int64  `json:\"id\"`\n\tZoneID       int64  `json:\"zoneID\"`\n\tType         string `json:\"type\"`\n\tName         string `json:\"name\"`\n\tPort         int    `json:\"port\"`\n\tWeight       int    `json:\"weight\"`\n\tPriority     int    `json:\"priority\"`\n\tData         string `json:\"data\"`\n\tExpandedData string `json:\"expandedData\"`\n\tFlags        int    `json:\"flags,omitempty\"`\n\tTag          string `json:\"tag,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/flexcdn/api_update_ssl_cert.go",
    "content": "package flexcdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype UpdateSSLCertRequest struct {\n\tSSLCertId   int64    `json:\"sslCertId\"`\n\tIsOn        bool     `json:\"isOn\"`\n\tName        string   `json:\"name\"`\n\tDescription string   `json:\"description\"`\n\tServerName  string   `json:\"serverName\"`\n\tIsCA        bool     `json:\"isCA\"`\n\tCertData    string   `json:\"certData\"`\n\tKeyData     string   `json:\"keyData\"`\n\tTimeBeginAt int64    `json:\"timeBeginAt\"`\n\tTimeEndAt   int64    `json:\"timeEndAt\"`\n\tDNSNames    []string `json:\"dnsNames\"`\n\tCommonNames []string `json:\"commonNames\"`\n}\n\ntype UpdateSSLCertResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) UpdateSSLCert(req *UpdateSSLCertRequest) (*UpdateSSLCertResponse, error) {\n\treturn c.UpdateSSLCertWithContext(context.Background(), req)\n}\n\nfunc (c *Client) UpdateSSLCertWithContext(ctx context.Context, req *UpdateSSLCertRequest) (*UpdateSSLCertResponse, error) {\n\tif err := c.ensureAccessTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, \"/SSLCertService/updateSSLCert\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateSSLCertResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/flexcdn/client.go",
    "content": "package flexcdn\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tapiRole     string\n\taccessKeyId string\n\taccessKey   string\n\n\taccessToken    string\n\taccessTokenExp time.Time\n\taccessTokenMtx sync.Mutex\n\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl, apiRole, accessKeyId, accessKey string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif apiRole == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiRole\")\n\t}\n\tif apiRole != \"user\" && apiRole != \"admin\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid apiRole\")\n\t}\n\tif accessKeyId == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset accessKeyId\")\n\t}\n\tif accessKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset accessKey\")\n\t}\n\n\tclient := &Client{\n\t\tapiRole:     apiRole,\n\t\taccessKeyId: accessKeyId,\n\t\taccessKey:   accessKey,\n\t}\n\tclient.client = resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")).\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\tif client.accessToken != \"\" {\n\t\t\t\treq.Header.Set(\"X-Cloud-Access-Token\", client.accessToken)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\treturn client, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tcode := res.GetCode(); tcode != 200 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: code='%d', message='%s'\", tcode, res.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) ensureAccessTokenExists() error {\n\tc.accessTokenMtx.Lock()\n\tdefer c.accessTokenMtx.Unlock()\n\tif c.accessToken != \"\" && c.accessTokenExp.After(time.Now()) {\n\t\treturn nil\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, \"/APIAccessTokenService/getAPIAccessToken\")\n\tif err != nil {\n\t\treturn err\n\t} else {\n\t\thttpreq.SetBody(map[string]string{\n\t\t\t\"type\":        c.apiRole,\n\t\t\t\"accessKeyId\": c.accessKeyId,\n\t\t\t\"accessKey\":   c.accessKey,\n\t\t})\n\t}\n\n\ttype getAPIAccessTokenResponse struct {\n\t\tsdkResponseBase\n\t\tData *struct {\n\t\t\tToken     string `json:\"token\"`\n\t\t\tExpiresAt int64  `json:\"expiresAt\"`\n\t\t} `json:\"data,omitempty\"`\n\t}\n\n\tresult := &getAPIAccessTokenResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn err\n\t} else if code := result.GetCode(); code != 200 {\n\t\treturn fmt.Errorf(\"sdkerr: failed to get flexcdn access token: code='%d', message='%s'\", code, result.GetMessage())\n\t} else {\n\t\tc.accessToken = result.Data.Token\n\t\tc.accessTokenExp = time.Unix(result.Data.ExpiresAt, 0)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/flexcdn/types.go",
    "content": "package flexcdn\n\ntype sdkResponse interface {\n\tGetCode() int\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() int {\n\treturn r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\treturn r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n"
  },
  {
    "path": "pkg/sdk3rd/flyio/api_import_custom_certificate.go",
    "content": "package flyio\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype ImportCustomCertificateRequest struct {\n\tAppName    string `json:\"-\"`\n\tHostname   string `json:\"hostname\"`\n\tFullchain  string `json:\"fullchain\"`\n\tPrivateKey string `json:\"private_key\"`\n}\n\ntype ImportCustomCertificateResponse struct {\n\tsdkResponseBase\n\n\tHostname     string `json:\"hostname\"`\n\tConfigured   bool   `json:\"configured\"`\n\tStatus       string `json:\"status\"`\n\tCertificates []*struct {\n\t\tSource    string `json:\"source\"`\n\t\tStatus    string `json:\"status\"`\n\t\tCreatedAt string `json:\"created_at\"`\n\t\tExpiresAt string `json:\"expires_at\"`\n\t\tIssuer    string `json:\"issuer\"`\n\t} `json:\"certificates\"`\n}\n\nfunc (c *Client) ImportCustomCertificate(req *ImportCustomCertificateRequest) (*ImportCustomCertificateResponse, error) {\n\treturn c.ImportCustomCertificateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) ImportCustomCertificateWithContext(ctx context.Context, req *ImportCustomCertificateRequest) (*ImportCustomCertificateResponse, error) {\n\tif req.AppName == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset appName\")\n\t}\n\n\tpath := fmt.Sprintf(\"/apps/%s/certificates/custom\", url.PathEscape(req.AppName))\n\thttpreq, err := c.newRequest(http.MethodPost, path)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ImportCustomCertificateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/flyio/client.go",
    "content": "package flyio\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(apiToken string) (*Client, error) {\n\tif apiToken == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiToken\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(\"https://api.machines.dev/v1\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Authorization\", \"Bearer \"+apiToken).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent)\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/flyio/types.go",
    "content": "package flyio\n\ntype sdkResponse interface {\n\tGetError() string\n}\n\ntype sdkResponseBase struct {\n\tError *string `json:\"error,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetError() string {\n\tif r.Error == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Error\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n"
  },
  {
    "path": "pkg/sdk3rd/gcore/endpoint.go",
    "content": "package common\n\nconst BASE_URL = \"https://api.gcore.com\"\n"
  },
  {
    "path": "pkg/sdk3rd/gcore/signer.go",
    "content": "package common\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/G-Core/gcorelabscdn-go/gcore\"\n)\n\ntype AuthRequestSigner struct {\n\tapiToken string\n}\n\nvar _ gcore.RequestSigner = (*AuthRequestSigner)(nil)\n\nfunc NewAuthRequestSigner(apiToken string) *AuthRequestSigner {\n\treturn &AuthRequestSigner{\n\t\tapiToken: apiToken,\n\t}\n}\n\nfunc (s *AuthRequestSigner) Sign(req *http.Request) error {\n\treq.Header.Set(\"Authorization\", \"APIKey \"+s.apiToken)\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/gname/api_add_domain_resolution.go",
    "content": "package gname\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n)\n\ntype AddDomainResolutionRequest struct {\n\tZoneName    *string `json:\"ym,omitempty\"`\n\tRecordType  *string `json:\"lx,omitempty\"`\n\tRecordName  *string `json:\"zj,omitempty\"`\n\tRecordValue *string `json:\"jlz,omitempty\"`\n\tMX          *int32  `json:\"mx,omitempty\"`\n\tTTL         *int32  `json:\"ttl,omitempty\"`\n}\n\ntype AddDomainResolutionResponse struct {\n\tsdkResponseBase\n\n\tData json.Number `json:\"data\"`\n}\n\nfunc (c *Client) AddDomainResolution(req *AddDomainResolutionRequest) (*AddDomainResolutionResponse, error) {\n\treturn c.AddDomainResolutionWithContext(context.Background(), req)\n}\n\nfunc (c *Client) AddDomainResolutionWithContext(ctx context.Context, req *AddDomainResolutionRequest) (*AddDomainResolutionResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/api/resolution/add\", req)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &AddDomainResolutionResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/gname/api_delete_domain_resolution.go",
    "content": "package gname\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype DeleteDomainResolutionRequest struct {\n\tZoneName *string `json:\"ym,omitempty\"`\n\tRecordID *int64  `json:\"jxid,omitempty\"`\n}\n\ntype DeleteDomainResolutionResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) DeleteDomainResolution(req *DeleteDomainResolutionRequest) (*DeleteDomainResolutionResponse, error) {\n\treturn c.DeleteDomainResolutionWithContext(context.Background(), req)\n}\n\nfunc (c *Client) DeleteDomainResolutionWithContext(ctx context.Context, req *DeleteDomainResolutionRequest) (*DeleteDomainResolutionResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/api/resolution/delete\", req)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &DeleteDomainResolutionResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/gname/api_list_domain_resolution.go",
    "content": "package gname\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype ListDomainResolutionRequest struct {\n\tZoneName *string `json:\"ym,omitempty\"`\n\tPage     *int32  `json:\"page,omitempty\"`\n\tPageSize *int32  `json:\"limit,omitempty\"`\n}\n\ntype ListDomainResolutionResponse struct {\n\tsdkResponseBase\n\n\tCount    int32                        `json:\"count\"`\n\tData     []*DomainResolutionRecordord `json:\"data\"`\n\tPage     int32                        `json:\"page\"`\n\tPageSize int32                        `json:\"pagesize\"`\n}\n\nfunc (c *Client) ListDomainResolution(req *ListDomainResolutionRequest) (*ListDomainResolutionResponse, error) {\n\treturn c.ListDomainResolutionWithContext(context.Background(), req)\n}\n\nfunc (c *Client) ListDomainResolutionWithContext(ctx context.Context, req *ListDomainResolutionRequest) (*ListDomainResolutionResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/api/resolution/list\", req)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ListDomainResolutionResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/gname/api_modify_domain_resolution.go",
    "content": "package gname\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype ModifyDomainResolutionRequest struct {\n\tID          *int64  `json:\"jxid,omitempty\"`\n\tZoneName    *string `json:\"ym,omitempty\"`\n\tRecordType  *string `json:\"lx,omitempty\"`\n\tRecordName  *string `json:\"zj,omitempty\"`\n\tRecordValue *string `json:\"jlz,omitempty\"`\n\tMX          *int32  `json:\"mx,omitempty\"`\n\tTTL         *int32  `json:\"ttl,omitempty\"`\n}\n\ntype ModifyDomainResolutionResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) ModifyDomainResolution(req *ModifyDomainResolutionRequest) (*ModifyDomainResolutionResponse, error) {\n\treturn c.ModifyDomainResolutionWithContext(context.Background(), req)\n}\n\nfunc (c *Client) ModifyDomainResolutionWithContext(ctx context.Context, req *ModifyDomainResolutionRequest) (*ModifyDomainResolutionResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/api/resolution/edit\", req)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ModifyDomainResolutionResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/gname/client.go",
    "content": "package gname\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tappId  string\n\tappKey string\n\n\tclient *resty.Client\n}\n\nfunc NewClient(appId, appKey string) (*Client, error) {\n\tif appId == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset appId\")\n\t}\n\tif appKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset appKey\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(\"https://api.gname.com\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/x-www-form-urlencoded\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent)\n\n\treturn &Client{\n\t\tappId:  appId,\n\t\tappKey: appKey,\n\t\tclient: client,\n\t}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string, params any) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\tdata := make(map[string]string)\n\tif params != nil {\n\t\ttemp := make(map[string]any)\n\t\tjsonb, _ := json.Marshal(params)\n\t\tjson.Unmarshal(jsonb, &temp)\n\t\tfor k, v := range temp {\n\t\t\tif v == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdata[k] = fmt.Sprintf(\"%v\", v)\n\t\t}\n\t}\n\n\tdata[\"appid\"] = c.appId\n\tdata[\"gntime\"] = fmt.Sprintf(\"%d\", time.Now().Unix())\n\tdata[\"gntoken\"] = generateSignature(data, c.appKey)\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treq.SetFormData(data)\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetBody` or `req.SetFormData` HERE! USE `newRequest` INSTEAD.\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tcode := res.GetCode(); tcode != 1 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: code='%d', message='%s'\", tcode, res.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n\nfunc generateSignature(params map[string]string, appKey string) string {\n\t// Step 1: Sort parameters by ASCII order\n\tvar keys []string\n\tfor k := range params {\n\t\tkeys = append(keys, k)\n\t}\n\tsort.Strings(keys)\n\n\t// Step 2: Create string A with URL-encoded values\n\tvar pairs []string\n\tfor _, k := range keys {\n\t\tencodedValue := url.QueryEscape(params[k])\n\t\tpairs = append(pairs, fmt.Sprintf(\"%s=%s\", k, encodedValue))\n\t}\n\tstringA := strings.Join(pairs, \"&\")\n\n\t// Step 3: Append appkey to create string B\n\tstringB := stringA + appKey\n\n\t// Step 4: Calculate MD5 and convert to uppercase\n\thash := md5.Sum([]byte(stringB))\n\treturn strings.ToUpper(fmt.Sprintf(\"%x\", hash))\n}\n"
  },
  {
    "path": "pkg/sdk3rd/gname/types.go",
    "content": "package gname\n\nimport \"encoding/json\"\n\ntype sdkResponse interface {\n\tGetCode() int\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"msg\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() int {\n\treturn r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\treturn r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype DomainResolutionRecordord struct {\n\tID          json.Number `json:\"id\"`\n\tZoneName    string      `json:\"ym\"`\n\tRecordType  string      `json:\"lx\"`\n\tRecordName  string      `json:\"zjt\"`\n\tRecordValue string      `json:\"jxz\"`\n\tMX          int32       `json:\"mx\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/goedge/api_update_ssl_cert.go",
    "content": "package goedge\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype UpdateSSLCertRequest struct {\n\tSSLCertId   int64    `json:\"sslCertId\"`\n\tIsOn        bool     `json:\"isOn\"`\n\tName        string   `json:\"name\"`\n\tDescription string   `json:\"description\"`\n\tServerName  string   `json:\"serverName\"`\n\tIsCA        bool     `json:\"isCA\"`\n\tCertData    string   `json:\"certData\"`\n\tKeyData     string   `json:\"keyData\"`\n\tTimeBeginAt int64    `json:\"timeBeginAt\"`\n\tTimeEndAt   int64    `json:\"timeEndAt\"`\n\tDNSNames    []string `json:\"dnsNames\"`\n\tCommonNames []string `json:\"commonNames\"`\n}\n\ntype UpdateSSLCertResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) UpdateSSLCert(req *UpdateSSLCertRequest) (*UpdateSSLCertResponse, error) {\n\treturn c.UpdateSSLCertWithContext(context.Background(), req)\n}\n\nfunc (c *Client) UpdateSSLCertWithContext(ctx context.Context, req *UpdateSSLCertRequest) (*UpdateSSLCertResponse, error) {\n\tif err := c.ensureAccessTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, \"/SSLCertService/updateSSLCert\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateSSLCertResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/goedge/client.go",
    "content": "package goedge\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tapiRole     string\n\taccessKeyId string\n\taccessKey   string\n\n\taccessToken    string\n\taccessTokenExp time.Time\n\taccessTokenMtx sync.Mutex\n\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl, apiRole, accessKeyId, accessKey string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif apiRole == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiRole\")\n\t}\n\tif apiRole != \"user\" && apiRole != \"admin\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid apiRole\")\n\t}\n\tif accessKeyId == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset accessKeyId\")\n\t}\n\tif accessKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset accessKey\")\n\t}\n\n\tclient := &Client{\n\t\tapiRole:     apiRole,\n\t\taccessKeyId: accessKeyId,\n\t\taccessKey:   accessKey,\n\t}\n\tclient.client = resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")).\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\tif client.accessToken != \"\" {\n\t\t\t\treq.Header.Set(\"X-Edge-Access-Token\", client.accessToken)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\treturn client, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tcode := res.GetCode(); tcode != 200 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: code='%d', message='%s'\", tcode, res.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) ensureAccessTokenExists() error {\n\tc.accessTokenMtx.Lock()\n\tdefer c.accessTokenMtx.Unlock()\n\tif c.accessToken != \"\" && c.accessTokenExp.After(time.Now()) {\n\t\treturn nil\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, \"/APIAccessTokenService/getAPIAccessToken\")\n\tif err != nil {\n\t\treturn err\n\t} else {\n\t\thttpreq.SetBody(map[string]string{\n\t\t\t\"type\":        c.apiRole,\n\t\t\t\"accessKeyId\": c.accessKeyId,\n\t\t\t\"accessKey\":   c.accessKey,\n\t\t})\n\t}\n\n\ttype getAPIAccessTokenResponse struct {\n\t\tsdkResponseBase\n\t\tData *struct {\n\t\t\tToken     string `json:\"token\"`\n\t\t\tExpiresAt int64  `json:\"expiresAt\"`\n\t\t} `json:\"data,omitempty\"`\n\t}\n\n\tresult := &getAPIAccessTokenResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn err\n\t} else if code := result.GetCode(); code != 200 {\n\t\treturn fmt.Errorf(\"sdkerr: failed to get goedge access token: code='%d', message='%s'\", code, result.GetMessage())\n\t} else {\n\t\tc.accessToken = result.Data.Token\n\t\tc.accessTokenExp = time.Unix(result.Data.ExpiresAt, 0)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/goedge/types.go",
    "content": "package goedge\n\ntype sdkResponse interface {\n\tGetCode() int\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() int {\n\treturn r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\treturn r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n"
  },
  {
    "path": "pkg/sdk3rd/lecdn/v3/client/api_update_certificate.go",
    "content": "package client\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype UpdateCertificateRequest struct {\n\tName        string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tType        string `json:\"type\"`\n\tSSLPEM      string `json:\"ssl_pem\"`\n\tSSLKey      string `json:\"ssl_key\"`\n\tAutoRenewal bool   `json:\"auto_renewal\"`\n}\n\ntype UpdateCertificateResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) UpdateCertificate(certId int64, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {\n\treturn c.UpdateCertificateWithContext(context.Background(), certId, req)\n}\n\nfunc (c *Client) UpdateCertificateWithContext(ctx context.Context, certId int64, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {\n\tif certId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset certId\")\n\t}\n\n\tif err := c.ensureAccessTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf(\"/certificate/%d\", certId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateCertificateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/lecdn/v3/client/client.go",
    "content": "package client\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tusername string\n\tpassword string\n\n\taccessToken    string\n\taccessTokenMtx sync.Mutex\n\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl, username, password string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif username == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset username\")\n\t}\n\tif password == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset password\")\n\t}\n\n\tclient := &Client{\n\t\tusername: username,\n\t\tpassword: password,\n\t}\n\tclient.client = resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")+\"/prod-api\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\tif client.accessToken != \"\" {\n\t\t\t\treq.Header.Set(\"Authorization\", \"Bearer \"+client.accessToken)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\treturn client, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tcode := res.GetCode(); tcode != 200 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: code='%d', message='%s'\", tcode, res.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) ensureAccessTokenExists() error {\n\tc.accessTokenMtx.Lock()\n\tdefer c.accessTokenMtx.Unlock()\n\tif c.accessToken != \"\" {\n\t\treturn nil\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, \"/auth/login\")\n\tif err != nil {\n\t\treturn err\n\t} else {\n\t\thttpreq.SetBody(map[string]string{\n\t\t\t\"email\":    c.username,\n\t\t\t\"username\": c.username,\n\t\t\t\"password\": c.password,\n\t\t})\n\t}\n\n\ttype loginResponse struct {\n\t\tsdkResponseBase\n\t\tData *struct {\n\t\t\tUserId   int64  `json:\"user_id\"`\n\t\t\tUsername string `json:\"username\"`\n\t\t\tToken    string `json:\"token\"`\n\t\t} `json:\"data,omitempty\"`\n\t}\n\n\tresult := &loginResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn err\n\t} else {\n\t\tc.accessToken = result.Data.Token\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/lecdn/v3/client/types.go",
    "content": "package client\n\ntype sdkResponse interface {\n\tGetCode() int\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"msg\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() int {\n\treturn r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\treturn r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n"
  },
  {
    "path": "pkg/sdk3rd/lecdn/v3/master/api_update_certificate.go",
    "content": "package master\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype UpdateCertificateRequest struct {\n\tClientId    int64  `json:\"client_id\"`\n\tName        string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tType        string `json:\"type\"`\n\tSSLPEM      string `json:\"ssl_pem\"`\n\tSSLKey      string `json:\"ssl_key\"`\n\tAutoRenewal bool   `json:\"auto_renewal\"`\n}\n\ntype UpdateCertificateResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) UpdateCertificate(certId int64, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {\n\treturn c.UpdateCertificateWithContext(context.Background(), certId, req)\n}\n\nfunc (c *Client) UpdateCertificateWithContext(ctx context.Context, certId int64, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {\n\tif certId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset certId\")\n\t}\n\n\tif err := c.ensureAccessTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf(\"/certificate/%d\", certId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateCertificateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/lecdn/v3/master/client.go",
    "content": "package master\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tusername string\n\tpassword string\n\n\taccessToken    string\n\taccessTokenMtx sync.Mutex\n\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl, username, password string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif username == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset username\")\n\t}\n\tif password == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset password\")\n\t}\n\n\tclient := &Client{\n\t\tusername: username,\n\t\tpassword: password,\n\t}\n\tclient.client = resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")+\"/prod-api\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\tif client.accessToken != \"\" {\n\t\t\t\treq.Header.Set(\"Authorization\", \"Bearer \"+client.accessToken)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\treturn client, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tcode := res.GetCode(); tcode != 200 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: code='%d', message='%s'\", tcode, res.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) ensureAccessTokenExists() error {\n\tc.accessTokenMtx.Lock()\n\tdefer c.accessTokenMtx.Unlock()\n\tif c.accessToken != \"\" {\n\t\treturn nil\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, \"/auth/login\")\n\tif err != nil {\n\t\treturn err\n\t} else {\n\t\thttpreq.SetBody(map[string]string{\n\t\t\t\"username\": c.username,\n\t\t\t\"password\": c.password,\n\t\t})\n\t}\n\n\ttype loginResponse struct {\n\t\tsdkResponseBase\n\t\tData *struct {\n\t\t\tUserId   int64  `json:\"user_id\"`\n\t\t\tUsername string `json:\"username\"`\n\t\t\tToken    string `json:\"token\"`\n\t\t} `json:\"data,omitempty\"`\n\t}\n\n\tresult := &loginResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn err\n\t} else {\n\t\tc.accessToken = result.Data.Token\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/lecdn/v3/master/types.go",
    "content": "package master\n\ntype sdkResponse interface {\n\tGetCode() int\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() int {\n\treturn r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\treturn r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n"
  },
  {
    "path": "pkg/sdk3rd/netlify/api_provision_site_tls_certificate.go",
    "content": "package netlify\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype ProvisionSiteTLSCertificateParams struct {\n\tCertificate    string `json:\"certificate\"`\n\tCACertificates string `json:\"ca_certificates\"`\n\tKey            string `json:\"key\"`\n}\n\ntype ProvisionSiteTLSCertificateResponse struct {\n\tsdkResponseBase\n\tDomains   []string `json:\"domains,omitempty\"`\n\tState     string   `json:\"state,omitempty\"`\n\tExpiresAt string   `json:\"expires_at,omitempty\"`\n\tCreatedAt string   `json:\"created_at,omitempty\"`\n\tUpdatedAt string   `json:\"updated_at,omitempty\"`\n}\n\nfunc (c *Client) ProvisionSiteTLSCertificate(siteId string, req *ProvisionSiteTLSCertificateParams) (*ProvisionSiteTLSCertificateResponse, error) {\n\treturn c.ProvisionSiteTLSCertificateWithContext(context.Background(), siteId, req)\n}\n\nfunc (c *Client) ProvisionSiteTLSCertificateWithContext(ctx context.Context, siteId string, req *ProvisionSiteTLSCertificateParams) (*ProvisionSiteTLSCertificateResponse, error) {\n\tif siteId == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset siteId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf(\"/sites/%s/ssl\", url.PathEscape(siteId)))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetQueryParams(map[string]string{\n\t\t\t\"certificate\":     req.Certificate,\n\t\t\t\"ca_certificates\": req.CACertificates,\n\t\t\t\"key\":             req.Key,\n\t\t})\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ProvisionSiteTLSCertificateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/netlify/client.go",
    "content": "package netlify\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(apiToken string) (*Client, error) {\n\tif apiToken == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiToken\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(\"https://api.netlify.com/api/v1\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Authorization\", \"Bearer \"+apiToken).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent)\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tcode := res.GetCode(); tcode != 0 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: code='%d', message='%s'\", tcode, res.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/netlify/types.go",
    "content": "package netlify\n\ntype sdkResponse interface {\n\tGetCode() int\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    *int    `json:\"code,omitempty\"`\n\tMessage *string `json:\"message,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() int {\n\tif r.Code == nil {\n\t\treturn 0\n\t}\n\n\treturn *r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n"
  },
  {
    "path": "pkg/sdk3rd/nginxproxymanager/api_nginx_create_certificate.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype NginxCreateCertificateRequest struct {\n\tProvider string `json:\"provider\"`\n\tNiceName string `json:\"nice_name\"`\n}\n\ntype NginxCreateCertificateResponse struct {\n\tCertificateRecord\n}\n\nfunc (c *Client) NginxCreateCertificate(req *NginxCreateCertificateRequest) (*NginxCreateCertificateResponse, error) {\n\treturn c.NginxCreateCertificateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) NginxCreateCertificateWithContext(ctx context.Context, req *NginxCreateCertificateRequest) (*NginxCreateCertificateResponse, error) {\n\tif err := c.ensureJwtTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, \"/nginx/certificates\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &NginxCreateCertificateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/nginxproxymanager/api_nginx_list_certificates.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype NginxListCertificatesRequest struct {\n\tExpand *string `json:\"expand,omitempty\" url:\"expand,omitempty\"`\n}\n\ntype NginxListCertificatesResponse = []*CertificateRecord\n\nfunc (c *Client) NginxListCertificates(req *NginxListCertificatesRequest) (*NginxListCertificatesResponse, error) {\n\treturn c.NginxListCertificatesWithContext(context.Background(), req)\n}\n\nfunc (c *Client) NginxListCertificatesWithContext(ctx context.Context, req *NginxListCertificatesRequest) (*NginxListCertificatesResponse, error) {\n\tif err := c.ensureJwtTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, \"/nginx/certificates\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &NginxListCertificatesResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/nginxproxymanager/api_nginx_list_dead_hosts.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype NginxListDeadHostsRequest struct {\n\tExpand *string `json:\"expand,omitempty\" url:\"expand,omitempty\"`\n}\n\ntype NginxListDeadHostsResponse = []*DeadHostRecord\n\nfunc (c *Client) NginxListDeadHosts(req *NginxListDeadHostsRequest) (*NginxListDeadHostsResponse, error) {\n\treturn c.NginxListDeadHostsWithContext(context.Background(), req)\n}\n\nfunc (c *Client) NginxListDeadHostsWithContext(ctx context.Context, req *NginxListDeadHostsRequest) (*NginxListDeadHostsResponse, error) {\n\tif err := c.ensureJwtTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, \"/nginx/dead-hosts\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &NginxListDeadHostsResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/nginxproxymanager/api_nginx_list_proxy_hosts.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype NginxListProxyHostsRequest struct {\n\tExpand *string `json:\"expand,omitempty\" url:\"expand,omitempty\"`\n}\n\ntype NginxListProxyHostsResponse = []*ProxyHostRecord\n\nfunc (c *Client) NginxListProxyHosts(req *NginxListProxyHostsRequest) (*NginxListProxyHostsResponse, error) {\n\treturn c.NginxListProxyHostsWithContext(context.Background(), req)\n}\n\nfunc (c *Client) NginxListProxyHostsWithContext(ctx context.Context, req *NginxListProxyHostsRequest) (*NginxListProxyHostsResponse, error) {\n\tif err := c.ensureJwtTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, \"/nginx/proxy-hosts\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &NginxListProxyHostsResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/nginxproxymanager/api_nginx_list_redirection_hosts.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype NginxListRedirectionHostsRequest struct {\n\tExpand *string `json:\"expand,omitempty\" url:\"expand,omitempty\"`\n}\n\ntype NginxListRedirectionHostsResponse = []*RedirectionHostRecord\n\nfunc (c *Client) NginxListRedirectionHosts(req *NginxListRedirectionHostsRequest) (*NginxListRedirectionHostsResponse, error) {\n\treturn c.NginxListRedirectionHostsWithContext(context.Background(), req)\n}\n\nfunc (c *Client) NginxListRedirectionHostsWithContext(ctx context.Context, req *NginxListRedirectionHostsRequest) (*NginxListRedirectionHostsResponse, error) {\n\tif err := c.ensureJwtTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, \"/nginx/redirection-hosts\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &NginxListRedirectionHostsResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/nginxproxymanager/api_nginx_list_streams.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype NginxListStreamsRequest struct {\n\tExpand *string `json:\"expand,omitempty\" url:\"expand,omitempty\"`\n}\n\ntype NginxListStreamsResponse = []*StreamHostRecord\n\nfunc (c *Client) NginxListStreams(req *NginxListStreamsRequest) (*NginxListStreamsResponse, error) {\n\treturn c.NginxListStreamsWithContext(context.Background(), req)\n}\n\nfunc (c *Client) NginxListStreamsWithContext(ctx context.Context, req *NginxListStreamsRequest) (*NginxListStreamsResponse, error) {\n\tif err := c.ensureJwtTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, \"/nginx/streams\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &NginxListStreamsResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/nginxproxymanager/api_nginx_update_dead_host.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype NginxUpdateDeadHostRequest struct {\n\tCertificateId *int64 `json:\"certificate_id,omitempty\"`\n}\n\ntype NginxUpdateDeadHostResponse struct {\n\tDeadHostRecord\n}\n\nfunc (c *Client) NginxUpdateDeadHost(hostId int64, req *NginxUpdateDeadHostRequest) (*NginxUpdateDeadHostResponse, error) {\n\treturn c.NginxUpdateDeadHostWithContext(context.Background(), hostId, req)\n}\n\nfunc (c *Client) NginxUpdateDeadHostWithContext(ctx context.Context, hostId int64, req *NginxUpdateDeadHostRequest) (*NginxUpdateDeadHostResponse, error) {\n\tif hostId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset hostId\")\n\t}\n\n\tif err := c.ensureJwtTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf(\"/nginx/dead-hosts/%d\", hostId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &NginxUpdateDeadHostResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/nginxproxymanager/api_nginx_update_proxy_host.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype NginxUpdateProxyHostRequest struct {\n\tCertificateId *int64 `json:\"certificate_id,omitempty\"`\n}\n\ntype NginxUpdateProxyHostResponse struct {\n\tProxyHostRecord\n}\n\nfunc (c *Client) NginxUpdateProxyHost(hostId int64, req *NginxUpdateProxyHostRequest) (*NginxUpdateProxyHostResponse, error) {\n\treturn c.NginxUpdateProxyHostWithContext(context.Background(), hostId, req)\n}\n\nfunc (c *Client) NginxUpdateProxyHostWithContext(ctx context.Context, hostId int64, req *NginxUpdateProxyHostRequest) (*NginxUpdateProxyHostResponse, error) {\n\tif hostId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset hostId\")\n\t}\n\n\tif err := c.ensureJwtTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf(\"/nginx/proxy-hosts/%d\", hostId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &NginxUpdateProxyHostResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/nginxproxymanager/api_nginx_update_redirection_host.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype NginxUpdateRedirectionHostRequest struct {\n\tCertificateId *int64 `json:\"certificate_id,omitempty\"`\n}\n\ntype NginxUpdateRedirectionHostResponse struct {\n\tRedirectionHostRecord\n}\n\nfunc (c *Client) NginxUpdateRedirectionHost(hostId int64, req *NginxUpdateRedirectionHostRequest) (*NginxUpdateRedirectionHostResponse, error) {\n\treturn c.NginxUpdateRedirectionHostWithContext(context.Background(), hostId, req)\n}\n\nfunc (c *Client) NginxUpdateRedirectionHostWithContext(ctx context.Context, hostId int64, req *NginxUpdateRedirectionHostRequest) (*NginxUpdateRedirectionHostResponse, error) {\n\tif hostId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset hostId\")\n\t}\n\n\tif err := c.ensureJwtTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf(\"/nginx/redirection-hosts/%d\", hostId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &NginxUpdateRedirectionHostResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/nginxproxymanager/api_nginx_update_stream.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype NginxUpdateStreamRequest struct {\n\tCertificateId *int64 `json:\"certificate_id,omitempty\"`\n}\n\ntype NginxUpdateStreamResponse struct {\n\tStreamHostRecord\n}\n\nfunc (c *Client) NginxUpdateStream(hostId int64, req *NginxUpdateStreamRequest) (*NginxUpdateStreamResponse, error) {\n\treturn c.NginxUpdateStreamWithContext(context.Background(), hostId, req)\n}\n\nfunc (c *Client) NginxUpdateStreamWithContext(ctx context.Context, hostId int64, req *NginxUpdateStreamRequest) (*NginxUpdateStreamResponse, error) {\n\tif hostId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset hostId\")\n\t}\n\n\tif err := c.ensureJwtTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf(\"/nginx/streams/%d\", hostId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &NginxUpdateStreamResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/nginxproxymanager/api_nginx_upload_certificate.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n)\n\ntype NginxUploadCertificateRequest struct {\n\tCertificateMeta\n}\n\ntype NginxUploadCertificateResponse struct {\n\tCertificateMeta\n}\n\nfunc (c *Client) NginxUploadCertificate(certId int64, req *NginxUploadCertificateRequest) (*NginxUploadCertificateResponse, error) {\n\treturn c.NginxUploadCertificateWithContext(context.Background(), certId, req)\n}\n\nfunc (c *Client) NginxUploadCertificateWithContext(ctx context.Context, certId int64, req *NginxUploadCertificateRequest) (*NginxUploadCertificateResponse, error) {\n\tif certId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset certId\")\n\t}\n\n\tif err := c.ensureJwtTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf(\"/nginx/certificates/%d/upload\", certId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetFileReader(\"certificate\", \"certificate.pem\", strings.NewReader(req.Certificate))\n\t\thttpreq.SetFileReader(\"certificate_key\", \"privkey.pem\", strings.NewReader(req.CertificateKey))\n\t\thttpreq.SetFileReader(\"intermediate_certificate\", \"cabundle.pem\", strings.NewReader(req.IntermediateCertificate))\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &NginxUploadCertificateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/nginxproxymanager/api_settings_get_default_site.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype SettingsGetDefaultSiteRequest struct{}\n\ntype SettingsGetDefaultSiteResponse struct {\n\tId          string `json:\"id\"`\n\tName        string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tValue       string `json:\"value\"`\n\tMeta        struct {\n\t\tRedirect string `json:\"redirect\"`\n\t\tHtml     string `json:\"urhtmll\"`\n\t} `json:\"meta\"`\n}\n\nfunc (c *Client) SettingsGetDefaultSite(req *SettingsGetDefaultSiteRequest) (*SettingsGetDefaultSiteResponse, error) {\n\treturn c.SettingsGetDefaultSiteWithContext(context.Background(), req)\n}\n\nfunc (c *Client) SettingsGetDefaultSiteWithContext(ctx context.Context, req *SettingsGetDefaultSiteRequest) (*SettingsGetDefaultSiteResponse, error) {\n\tif err := c.ensureJwtTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, \"/settings/default-site\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SettingsGetDefaultSiteResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/nginxproxymanager/api_settings_set_default_site.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype SettingsSetDefaultSiteRequest struct {\n\tValue string `json:\"value\"`\n\tMeta  struct {\n\t\tRedirect string `json:\"redirect\"`\n\t\tHtml     string `json:\"html\"`\n\t} `json:\"meta\"`\n}\n\ntype SettingsSetDefaultSiteResponse struct{}\n\nfunc (c *Client) SettingsSetDefaultSite(req *SettingsSetDefaultSiteRequest) (*SettingsSetDefaultSiteResponse, error) {\n\treturn c.SettingsSetDefaultSiteWithContext(context.Background(), req)\n}\n\nfunc (c *Client) SettingsSetDefaultSiteWithContext(ctx context.Context, req *SettingsSetDefaultSiteRequest) (*SettingsSetDefaultSiteResponse, error) {\n\tif err := c.ensureJwtTokenExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPut, \"/settings/default-site\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SettingsSetDefaultSiteResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/nginxproxymanager/client.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tidentity string\n\tsecret   string\n\n\tjwtToken    string\n\tjwtTokenMtx sync.Mutex\n\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl, identity, secret string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif identity == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset identity\")\n\t}\n\tif secret == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset secret\")\n\t}\n\n\tclient := &Client{\n\t\tidentity: identity,\n\t\tsecret:   secret,\n\t}\n\tclient.client = resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")+\"/api\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\tif client.jwtToken != \"\" {\n\t\t\t\treq.Header.Set(\"Authorization\", \"Bearer \"+client.jwtToken)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\treturn client, nil\n}\n\nfunc NewClientWithJwtToken(serverUrl, jwtToken string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif jwtToken == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset jwtToken\")\n\t}\n\n\tclient := &Client{\n\t\tjwtToken: jwtToken,\n\t}\n\tclient.client = resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")+\"/api\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\tif client.jwtToken != \"\" {\n\t\t\t\treq.Header.Set(\"Authorization\", \"Bearer \"+client.jwtToken)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\treturn client, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res interface{}) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tvar errRes *sdkResponseBase\n\t\tif err := json.Unmarshal(resp.Body(), &errRes); err == nil {\n\t\t\tif terror := errRes.GetError(); terror != \"\" {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: error='%s'\", terror)\n\t\t\t}\n\t\t}\n\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) ensureJwtTokenExists() error {\n\tc.jwtTokenMtx.Lock()\n\tdefer c.jwtTokenMtx.Unlock()\n\tif c.jwtToken != \"\" {\n\t\treturn nil\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, \"/tokens\")\n\tif err != nil {\n\t\treturn err\n\t} else {\n\t\thttpreq.SetBody(map[string]string{\n\t\t\t\"identity\": c.identity,\n\t\t\t\"secret\":   c.secret,\n\t\t})\n\t}\n\n\ttype tokensResponse struct {\n\t\tsdkResponseBase\n\t\tToken   string `json:\"token\"`\n\t\tExpires string `json:\"expires\"`\n\t}\n\n\tresult := &tokensResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn err\n\t} else if terror := result.GetError(); terror != \"\" {\n\t\treturn fmt.Errorf(\"sdkerr: failed to create npm token: error='%s'\", terror)\n\t} else {\n\t\tc.jwtToken = result.Token\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/nginxproxymanager/types.go",
    "content": "package nginxproxymanager\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\ntype sdkResponse interface {\n\tGetError() string\n}\n\ntype sdkResponseBase struct {\n\tError json.RawMessage `json:\"error\"`\n}\n\nfunc (r *sdkResponseBase) GetError() string {\n\tif len(r.Error) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar errStr string\n\tif err := json.Unmarshal(r.Error, &errStr); err == nil {\n\t\treturn errStr\n\t}\n\n\ttype errObjType struct {\n\t\tCode    int    `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t}\n\tvar errObj errObjType\n\tif err := json.Unmarshal(r.Error, &errObj); err == nil && errObj.Message != \"\" {\n\t\tif errObj.Code != 0 {\n\t\t\treturn fmt.Sprintf(\"%d %s\", errObj.Code, errObj.Message)\n\t\t}\n\t\treturn errObj.Message\n\t}\n\n\tvar errMap map[string]interface{}\n\tif err := json.Unmarshal(r.Error, &errMap); err == nil {\n\t\tif message, ok := errMap[\"message\"].(string); ok {\n\t\t\treturn message\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype CertificateRecord struct {\n\tId          int64           `json:\"id\"`\n\tCreatedOn   string          `json:\"created_on\"`\n\tModifiedOn  string          `json:\"modified_on\"`\n\tProvider    string          `json:\"provider\"`\n\tNiceName    string          `json:\"nice_name\"`\n\tDomainNames []string        `json:\"domain_names\"`\n\tExpiresOn   string          `json:\"expires_on\"`\n\tMeta        CertificateMeta `json:\"meta\"`\n}\n\ntype CertificateMeta struct {\n\tCertificate             string `json:\"certificate\"`\n\tCertificateKey          string `json:\"certificate_key\"`\n\tIntermediateCertificate string `json:\"intermediate_certificate\"`\n}\n\ntype HostRecord struct {\n\tId            int64    `json:\"id\"`\n\tCreatedOn     string   `json:\"created_on\"`\n\tModifiedOn    string   `json:\"modified_on\"`\n\tDomainNames   []string `json:\"domain_names\"`\n\tCertificateId int64    `json:\"certificate_id\"`\n\tMeta          HostMeta `json:\"meta\"`\n\tEnabled       bool     `json:\"enabled\"`\n}\n\ntype HostMeta struct {\n\tNginxOnline bool `json:\"nginx_online\"`\n\tNginxErr    any  `json:\"nginx_err\"`\n}\n\ntype ProxyHostRecord struct {\n\tHostRecord\n\tForwardScheme  string `json:\"forward_scheme\"`\n\tForwardHost    string `json:\"forward_host\"`\n\tForwardPort    int32  `json:\"forward_port\"`\n\tSslForced      bool   `json:\"ssl_forced\"`\n\tHttp2Support   bool   `json:\"http2_support\"`\n\tHstsEnabled    bool   `json:\"hsts_enabled\"`\n\tHstsSubdomains bool   `json:\"hsts_subdomains\"`\n}\n\ntype RedirectionHostRecord struct {\n\tHostRecord\n\tForwardScheme     string `json:\"forward_scheme\"`\n\tForwardDomainName string `json:\"forward_domain_name\"`\n\tForwardHttpCode   int32  `json:\"forward_http_code\"`\n\tSslForced         bool   `json:\"ssl_forced\"`\n\tHttp2Support      bool   `json:\"http2_support\"`\n\tHstsEnabled       bool   `json:\"hsts_enabled\"`\n\tHstsSubdomains    bool   `json:\"hsts_subdomains\"`\n}\n\ntype StreamHostRecord struct {\n\tHostRecord\n\tForwardingHost string `json:\"forwarding_host\"`\n\tForwardingPort int32  `json:\"forwarding_port\"`\n\tIncomingPort   int32  `json:\"incoming_port\"`\n\tTcpForwarding  bool   `json:\"tcp_forwarding\"`\n\tUdpForwarding  bool   `json:\"udp_forwarding\"`\n}\n\ntype DeadHostRecord struct {\n\tHostRecord\n\tSslForced      bool `json:\"ssl_forced\"`\n\tHttp2Support   bool `json:\"http2_support\"`\n\tHstsEnabled    bool `json:\"hsts_enabled\"`\n\tHstsSubdomains bool `json:\"hsts_subdomains\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/qingcloud/dns/api_create_record.go",
    "content": "package dns\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype CreateRecordRequest struct {\n\tZoneName   *string                      `json:\"zone_name,omitempty\"`\n\tDomainName *string                      `json:\"domain_name,omitempty\"`\n\tViewId     *int32                       `json:\"view_id,omitempty\"`\n\tType       *string                      `json:\"type,omitempty\"`\n\tRecords    []*CreateRecordRequestRecord `json:\"record,omitempty\"`\n\tTtl        *int32                       `json:\"ttl,omitempty\"`\n\tMode       *int32                       `json:\"mode,omitempty\"`\n\tAutoMerge  *int32                       `json:\"auto_merge,omitempty\"`\n}\n\ntype CreateRecordRequestRecord struct {\n\tValues []*CreateRecordRequestRecordValue `json:\"values,omitempty\"`\n\tWeight *int32                            `json:\"weight,omitempty\"`\n}\n\ntype CreateRecordRequestRecordValue struct {\n\tValue  *string `json:\"value,omitempty\"`\n\tStatus *int32  `json:\"status,omitempty\"`\n}\n\ntype CreateRecordResponse struct {\n\tsdkResponseBase\n\tDomainName     *string                       `json:\"domain_name,omitempty\"`\n\tDomainRecordId *int64                        `json:\"domain_record_id,omitempty\"`\n\tViewId         *int64                        `json:\"view_id,omitempty\"`\n\tRecords        []*CreateRecordResponseRecord `json:\"records,omitempty\"`\n}\n\ntype CreateRecordResponseRecord struct {\n\tGroupId     *int64                             `json:\"group_id,omitempty\"`\n\tGroupStatus *int32                             `json:\"group_status,omitempty\"`\n\tValues      []*CreateRecordResponseRecordValue `json:\"value,omitempty\"`\n\tWeight      *int32                             `json:\"weight,omitempty\"`\n}\n\ntype CreateRecordResponseRecordValue struct {\n\tValueId *int64  `json:\"id,omitempty\"`\n\tValue   *string `json:\"value,omitempty\"`\n\tStatus  *int32  `json:\"status,omitempty\"`\n}\n\nfunc (c *Client) CreateRecord(req *CreateRecordRequest) (*CreateRecordResponse, error) {\n\treturn c.CreateRecordWithContext(context.Background(), req)\n}\n\nfunc (c *Client) CreateRecordWithContext(ctx context.Context, req *CreateRecordRequest) (*CreateRecordResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/v1/record/\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &CreateRecordResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/qingcloud/dns/api_delete_record.go",
    "content": "package dns\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype DeleteRecordResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) DeleteRecord(recordIds []*int64) (*DeleteRecordResponse, error) {\n\treturn c.DeleteRecordWithContext(context.Background(), recordIds)\n}\n\nfunc (c *Client) DeleteRecordWithContext(ctx context.Context, recordIds []*int64) (*DeleteRecordResponse, error) {\n\tif len(recordIds) == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset recordIds\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, \"/v1/change_record_status/\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(map[string]any{\n\t\t\t\"ids\":    recordIds,\n\t\t\t\"action\": \"delete\",\n\t\t\t\"target\": \"record\",\n\t\t})\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &DeleteRecordResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/qingcloud/dns/client.go",
    "content": "package dns\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(accessKeyId, secretAccessKey string) (*Client, error) {\n\tif accessKeyId == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset accessKeyId\")\n\t}\n\tif secretAccessKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset secretAccessKey\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(\"http://api.routewize.com\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"Host\", \"api.routewize.com\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\t// 生成时间\n\t\t\tdate := time.Now().UTC().Format(time.RFC1123)\n\n\t\t\t// 获取请求谓词\n\t\t\tverb := req.Method\n\n\t\t\t// 获取访问资源\n\t\t\tcanonicalizedResource := \"/\"\n\t\t\tif req.URL != nil {\n\t\t\t\tcanonicalizedResource = req.URL.Path\n\t\t\t\tif req.URL.RawQuery != \"\" {\n\t\t\t\t\tvalues, _ := url.ParseQuery(req.URL.RawQuery)\n\t\t\t\t\tcanonicalizedResource += \"?\" + values.Encode()\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 计算签名\n\t\t\tstringToSign := verb + \"\\n\" +\n\t\t\t\tdate + \"\\n\" +\n\t\t\t\tcanonicalizedResource\n\t\t\th := hmac.New(sha256.New, []byte(secretAccessKey))\n\t\t\th.Write([]byte(stringToSign))\n\t\t\tsign := base64.StdEncoding.EncodeToString(h.Sum(nil))\n\n\t\t\t// 设置请求头\n\t\t\treq.Header.Set(\"Date\", date)\n\t\t\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"QC-HMAC-SHA256 %s:%s\", accessKeyId, sign))\n\n\t\t\treturn nil\n\t\t})\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tcode := res.GetCode(); tcode != 0 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: code='%d', message='%s'\", tcode, res.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/qingcloud/dns/types.go",
    "content": "package dns\n\ntype sdkResponse interface {\n\tGetCode() int\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    *int    `json:\"code,omitempty\"`\n\tMessage *string `json:\"message,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() int {\n\tif r.Code == nil {\n\t\treturn 0\n\t}\n\n\treturn *r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype DnsRecord struct {\n\tGroupId     *int64            `json:\"group_id,omitempty\"`\n\tGroupStatus *int32            `json:\"group_status,omitempty\"`\n\tValue       []*DnsRecordValue `json:\"value,omitempty\"`\n\tWeight      *int32            `json:\"weight,omitempty\"`\n}\n\ntype DnsRecordValue struct {\n\tId    *int64  `json:\"id,omitempty\"`\n\tType  *string `json:\"type,omitempty\"`\n\tValue *string `json:\"value,omitempty\"`\n\tLine  *string `json:\"line,omitempty\"`\n\tTtl   *int32  `json:\"ttl,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/qiniu/auth.go",
    "content": "package qiniu\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/qiniu/go-sdk/v7/auth\"\n\t\"github.com/qiniu/go-sdk/v7/client\"\n)\n\ntype transport struct {\n\thttp.RoundTripper\n\tmac *auth.Credentials\n}\n\nfunc newTransport(mac *auth.Credentials, tr http.RoundTripper) *transport {\n\tif tr == nil {\n\t\ttr = client.DefaultTransport\n\t}\n\treturn &transport{tr, mac}\n}\n\nfunc (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {\n\ttoken, err := t.mac.SignRequestV2(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Authorization\", \"Qiniu \"+token)\n\treturn t.RoundTripper.RoundTrip(req)\n}\n"
  },
  {
    "path": "pkg/sdk3rd/qiniu/cdn.go",
    "content": "package qiniu\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/qiniu/go-sdk/v7/auth\"\n\t\"github.com/qiniu/go-sdk/v7/client\"\n)\n\ntype CdnManager struct {\n\tclient *client.Client\n}\n\nfunc NewCdnManager(mac *auth.Credentials) *CdnManager {\n\tif mac == nil {\n\t\tmac = auth.Default()\n\t}\n\n\tclient := &client.Client{Client: &http.Client{Transport: newTransport(mac, nil)}}\n\treturn &CdnManager{client: client}\n}\n\ntype GetDomainListResponse struct {\n\tCode    *int    `json:\"code,omitempty\"`\n\tError   *string `json:\"error,omitempty\"`\n\tMarker  string  `json:\"marker\"`\n\tDomains []*struct {\n\t\tName               string `json:\"name\"`\n\t\tType               string `json:\"type\"`\n\t\tCName              string `json:\"cname\"`\n\t\tOperatingState     string `json:\"operatingState\"`\n\t\tOperatingStateDesc string `json:\"operatingStateDesc\"`\n\t\tCreateAt           string `json:\"createAt\"`\n\t\tModifyAt           string `json:\"modifyAt\"`\n\t} `json:\"domains\"`\n}\n\nfunc (m *CdnManager) GetDomainList(ctx context.Context, marker string, limit int) (*GetDomainListResponse, error) {\n\tquery := url.Values{}\n\tif marker != \"\" {\n\t\tquery.Set(\"marker\", marker)\n\t}\n\tif limit > 0 {\n\t\tquery.Set(\"limit\", fmt.Sprintf(\"%d\", limit))\n\t}\n\n\tresp := new(GetDomainListResponse)\n\tif err := m.client.Call(ctx, resp, http.MethodGet, urlf(\"domain?%s\", query.Encode()), nil); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\ntype GetDomainInfoResponse struct {\n\tCode  *int    `json:\"code,omitempty\"`\n\tError *string `json:\"error,omitempty\"`\n\tName  string  `json:\"name\"`\n\tType  string  `json:\"type\"`\n\tCName string  `json:\"cname\"`\n\tHttps *struct {\n\t\tCertID      string `json:\"certId\"`\n\t\tForceHttps  bool   `json:\"forceHttps\"`\n\t\tHttp2Enable bool   `json:\"http2Enable\"`\n\t} `json:\"https\"`\n\tPareDomain         string `json:\"pareDomain\"`\n\tOperationType      string `json:\"operationType\"`\n\tOperatingState     string `json:\"operatingState\"`\n\tOperatingStateDesc string `json:\"operatingStateDesc\"`\n\tCreateAt           string `json:\"createAt\"`\n\tModifyAt           string `json:\"modifyAt\"`\n}\n\nfunc (m *CdnManager) GetDomainInfo(ctx context.Context, domain string) (*GetDomainInfoResponse, error) {\n\tresp := new(GetDomainInfoResponse)\n\tif err := m.client.Call(ctx, resp, http.MethodGet, urlf(\"domain/%s\", domain), nil); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\ntype ModifyDomainHttpsConfRequest struct {\n\tCertID      string `json:\"certId\"`\n\tForceHttps  bool   `json:\"forceHttps\"`\n\tHttp2Enable bool   `json:\"http2Enable\"`\n}\n\ntype ModifyDomainHttpsConfResponse struct {\n\tCode  *int    `json:\"code,omitempty\"`\n\tError *string `json:\"error,omitempty\"`\n}\n\nfunc (m *CdnManager) ModifyDomainHttpsConf(ctx context.Context, domain string, certId string, forceHttps bool, http2Enable bool) (*ModifyDomainHttpsConfResponse, error) {\n\treq := &ModifyDomainHttpsConfRequest{\n\t\tCertID:      certId,\n\t\tForceHttps:  forceHttps,\n\t\tHttp2Enable: http2Enable,\n\t}\n\tresp := new(ModifyDomainHttpsConfResponse)\n\tif err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf(\"domain/%s/httpsconf\", domain), nil, req); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\ntype EnableDomainHttpsRequest struct {\n\tCertID      string `json:\"certId\"`\n\tForceHttps  bool   `json:\"forceHttps\"`\n\tHttp2Enable bool   `json:\"http2Enable\"`\n}\n\ntype EnableDomainHttpsResponse struct {\n\tCode  *int    `json:\"code,omitempty\"`\n\tError *string `json:\"error,omitempty\"`\n}\n\nfunc (m *CdnManager) EnableDomainHttps(ctx context.Context, domain string, certId string, forceHttps bool, http2Enable bool) (*EnableDomainHttpsResponse, error) {\n\treq := &EnableDomainHttpsRequest{\n\t\tCertID:      certId,\n\t\tForceHttps:  forceHttps,\n\t\tHttp2Enable: http2Enable,\n\t}\n\tresp := new(EnableDomainHttpsResponse)\n\tif err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf(\"domain/%s/sslize\", domain), nil, req); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/qiniu/kodo.go",
    "content": "package qiniu\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/qiniu/go-sdk/v7/auth\"\n\t\"github.com/qiniu/go-sdk/v7/client\"\n)\n\ntype KodoManager struct {\n\tclient *client.Client\n}\n\nfunc NewKodoManager(mac *auth.Credentials) *KodoManager {\n\tif mac == nil {\n\t\tmac = auth.Default()\n\t}\n\n\tclient := &client.Client{Client: &http.Client{Transport: newTransport(mac, nil)}}\n\treturn &KodoManager{client: client}\n}\n\ntype BindBucketCertRequest struct {\n\tCertID string `json:\"certid\"`\n\tDomain string `json:\"domain\"`\n}\n\ntype BindBucketCertResponse struct {\n\tCode  *int    `json:\"code,omitempty\"`\n\tError *string `json:\"error,omitempty\"`\n}\n\nfunc (m *KodoManager) BindBucketCert(ctx context.Context, domain string, certId string) (*BindBucketCertResponse, error) {\n\treq := &BindBucketCertRequest{\n\t\tCertID: certId,\n\t\tDomain: domain,\n\t}\n\tresp := new(BindBucketCertResponse)\n\tif err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf(\"cert/bind\"), nil, req); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/qiniu/sslcert.go",
    "content": "package qiniu\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/qiniu/go-sdk/v7/auth\"\n\t\"github.com/qiniu/go-sdk/v7/client\"\n)\n\ntype SslCertManager struct {\n\tclient *client.Client\n}\n\nfunc NewSslCertManager(mac *auth.Credentials) *SslCertManager {\n\tif mac == nil {\n\t\tmac = auth.Default()\n\t}\n\n\tclient := &client.Client{Client: &http.Client{Transport: newTransport(mac, nil)}}\n\treturn &SslCertManager{client: client}\n}\n\ntype GetSslCertListResponse struct {\n\tCode  *int    `json:\"code,omitempty\"`\n\tError *string `json:\"error,omitempty\"`\n\tCerts []*struct {\n\t\tCertID           string   `json:\"certid\"`\n\t\tName             string   `json:\"name\"`\n\t\tCommonName       string   `json:\"common_name\"`\n\t\tDnsNames         []string `json:\"dnsnames\"`\n\t\tCreateTime       int64    `json:\"create_time\"`\n\t\tNotBefore        int64    `json:\"not_before\"`\n\t\tNotAfter         int64    `json:\"not_after\"`\n\t\tProductType      string   `json:\"product_type\"`\n\t\tProductShortName string   `json:\"product_short_name,omitempty\"`\n\t\tOrderId          string   `json:\"orderid,omitempty\"`\n\t\tCertType         string   `json:\"cert_type\"`\n\t\tEncrypt          string   `json:\"encrypt\"`\n\t\tEncryptParameter string   `json:\"encryptParameter,omitempty\"`\n\t\tEnable           bool     `json:\"enable\"`\n\t} `json:\"certs\"`\n\tMarker string `json:\"marker\"`\n}\n\nfunc (m *SslCertManager) GetSslCertList(ctx context.Context, marker string, limit int32) (*GetSslCertListResponse, error) {\n\tresp := new(GetSslCertListResponse)\n\tif err := m.client.Call(ctx, resp, http.MethodGet, urlf(\"sslcert?marker=%s&limit=%d\", url.QueryEscape(marker), limit), nil); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\ntype UploadSslCertRequest struct {\n\tName        string `json:\"name\"`\n\tCommonName  string `json:\"common_name\"`\n\tCertificate string `json:\"ca\"`\n\tPrivateKey  string `json:\"pri\"`\n}\n\ntype UploadSslCertResponse struct {\n\tCode   *int    `json:\"code,omitempty\"`\n\tError  *string `json:\"error,omitempty\"`\n\tCertID string  `json:\"certID\"`\n}\n\nfunc (m *SslCertManager) UploadSslCert(ctx context.Context, name string, commonName string, certificate string, privateKey string) (*UploadSslCertResponse, error) {\n\treq := &UploadSslCertRequest{\n\t\tName:        name,\n\t\tCommonName:  commonName,\n\t\tCertificate: certificate,\n\t\tPrivateKey:  privateKey,\n\t}\n\tresp := new(UploadSslCertResponse)\n\tif err := m.client.CallWithJson(ctx, resp, http.MethodPost, urlf(\"sslcert\"), nil, req); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/qiniu/util.go",
    "content": "package qiniu\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\nconst qiniuHost = \"https://api.qiniu.com\"\n\nfunc urlf(pathf string, pathargs ...any) string {\n\tpath := fmt.Sprintf(pathf, pathargs...)\n\tpath = strings.TrimPrefix(path, \"/\")\n\treturn qiniuHost + \"/\" + path\n}\n"
  },
  {
    "path": "pkg/sdk3rd/rainyun/api_rcdn_instance_ssl_bind.go",
    "content": "package rainyun\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype RcdnInstanceSslBindRequest struct {\n\tCertId  int64    `json:\"cert_id\"`\n\tDomains []string `json:\"domains\"`\n}\n\ntype RcdnInstanceSslBindResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) RcdnInstanceSslBind(instanceId int64, req *RcdnInstanceSslBindRequest) (*RcdnInstanceSslBindResponse, error) {\n\treturn c.RcdnInstanceSslBindWithContext(context.Background(), instanceId, req)\n}\n\nfunc (c *Client) RcdnInstanceSslBindWithContext(ctx context.Context, instanceId int64, req *RcdnInstanceSslBindRequest) (*RcdnInstanceSslBindResponse, error) {\n\tif instanceId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset instanceId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf(\"/product/rcdn/instance/%d/ssl_bind\", instanceId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &RcdnInstanceSslBindResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/rainyun/api_ssl_center_create.go",
    "content": "package rainyun\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype SslCenterCreateRequest struct {\n\tCert string `json:\"cert\"`\n\tKey  string `json:\"key\"`\n}\n\ntype SslCenterCreateResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) SslCenterCreate(req *SslCenterCreateRequest) (*SslCenterCreateResponse, error) {\n\treturn c.SslCenterCreateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) SslCenterCreateWithContext(ctx context.Context, req *SslCenterCreateRequest) (*SslCenterCreateResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/product/sslcenter/\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SslCenterCreateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/rainyun/api_ssl_center_get.go",
    "content": "package rainyun\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype SslCenterGetResponse struct {\n\tsdkResponseBase\n\n\tData *SslDetail `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) SslCenterGet(sslId int64) (*SslCenterGetResponse, error) {\n\treturn c.SslCenterGetWithContext(context.Background(), sslId)\n}\n\nfunc (c *Client) SslCenterGetWithContext(ctx context.Context, sslId int64) (*SslCenterGetResponse, error) {\n\tif sslId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset sslId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf(\"/product/sslcenter/%d\", sslId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SslCenterGetResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/rainyun/api_ssl_center_list.go",
    "content": "package rainyun\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n)\n\ntype SslCenterListFilters struct {\n\tDomain *string `json:\"Domain,omitempty\"`\n}\n\ntype SslCenterListRequest struct {\n\tFilters *SslCenterListFilters `json:\"columnFilters,omitempty\"`\n\tSort    []*string             `json:\"sort,omitempty\"`\n\tPage    *int32                `json:\"page,omitempty\"`\n\tPerPage *int32                `json:\"perPage,omitempty\"`\n}\n\ntype SslCenterListResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tTotalRecords int32        `json:\"TotalRecords\"`\n\t\tRecords      []*SslRecord `json:\"Records\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) SslCenterList(req *SslCenterListRequest) (*SslCenterListResponse, error) {\n\treturn c.SslCenterListWithContext(context.Background(), req)\n}\n\nfunc (c *Client) SslCenterListWithContext(ctx context.Context, req *SslCenterListRequest) (*SslCenterListResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/product/sslcenter\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tjsonb, _ := json.Marshal(req)\n\t\thttpreq.SetQueryParam(\"options\", string(jsonb))\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SslCenterListResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/rainyun/api_ssl_center_update.go",
    "content": "package rainyun\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype SslCenterUpdateRequest struct {\n\tCert string `json:\"cert\"`\n\tKey  string `json:\"key\"`\n}\n\ntype SslCenterUpdateResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) SslCenterUpdate(certId int64, req *SslCenterUpdateRequest) (*SslCenterUpdateResponse, error) {\n\treturn c.SslCenterUpdateWithContext(context.Background(), certId, req)\n}\n\nfunc (c *Client) SslCenterUpdateWithContext(ctx context.Context, certId int64, req *SslCenterUpdateRequest) (*SslCenterUpdateResponse, error) {\n\tif certId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset certId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf(\"/product/sslcenter/%d\", certId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SslCenterUpdateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/rainyun/client.go",
    "content": "package rainyun\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(apiKey string) (*Client, error) {\n\tif apiKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiKey\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(\"https://api.v2.rainyun.com\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetHeader(\"X-API-Key\", apiKey)\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tcode := res.GetCode(); tcode/100 != 2 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: code='%d', message='%s'\", tcode, res.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/rainyun/types.go",
    "content": "package rainyun\n\ntype sdkResponse interface {\n\tGetCode() int\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    *int    `json:\"code,omitempty\"`\n\tMessage *string `json:\"message,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() int {\n\tif r.Code == nil {\n\t\treturn 0\n\t}\n\n\treturn *r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype SslRecord struct {\n\tID         int64  `json:\"ID\"`\n\tUID        int64  `json:\"UID\"`\n\tDomain     string `json:\"Domain\"`\n\tIssuer     string `json:\"Issuer\"`\n\tStartDate  int64  `json:\"StartDate\"`\n\tExpireDate int64  `json:\"ExpDate\"`\n\tUploadTime int64  `json:\"UploadTime\"`\n}\n\ntype SslDetail struct {\n\tCert       string `json:\"Cert\"`\n\tKey        string `json:\"Key\"`\n\tDomain     string `json:\"DomainName\"`\n\tIssuer     string `json:\"Issuer\"`\n\tStartDate  int64  `json:\"StartDate\"`\n\tExpireDate int64  `json:\"ExpDate\"`\n\tRemainDays int64  `json:\"RemainDays\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ratpanel/api_set_cert_update.go",
    "content": "package ratpanel\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype CertUpdateRequest struct {\n\tCertId      int64    `json:\"id\"`\n\tType        string   `json:\"type\"`\n\tDomains     []string `json:\"domains\"`\n\tCertificate string   `json:\"cert\"`\n\tPrivateKey  string   `json:\"key\"`\n}\n\ntype CertUpdateResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) CertUpdate(req *CertUpdateRequest) (*CertUpdateResponse, error) {\n\treturn c.CertUpdateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) CertUpdateWithContext(ctx context.Context, req *CertUpdateRequest) (*CertUpdateResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf(\"/cert/cert/%d\", req.CertId))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &CertUpdateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ratpanel/api_set_setting_cert.go",
    "content": "package ratpanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype SetSettingCertRequest struct {\n\tCertificate string `json:\"cert\"`\n\tPrivateKey  string `json:\"key\"`\n}\n\ntype SetSettingCertResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) SetSettingCert(req *SetSettingCertRequest) (*SetSettingCertResponse, error) {\n\treturn c.SetSettingCertWithContext(context.Background(), req)\n}\n\nfunc (c *Client) SetSettingCertWithContext(ctx context.Context, req *SetSettingCertRequest) (*SetSettingCertResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/setting/cert\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SetSettingCertResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ratpanel/api_set_website_cert.go",
    "content": "package ratpanel\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype SetWebsiteCertRequest struct {\n\tSiteName    string `json:\"name\"`\n\tCertificate string `json:\"cert\"`\n\tPrivateKey  string `json:\"key\"`\n}\n\ntype SetWebsiteCertResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) SetWebsiteCert(req *SetWebsiteCertRequest) (*SetWebsiteCertResponse, error) {\n\treturn c.SetWebsiteCertWithContext(context.Background(), req)\n}\n\nfunc (c *Client) SetWebsiteCertWithContext(ctx context.Context, req *SetWebsiteCertRequest) (*SetWebsiteCertResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/website/cert\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &SetWebsiteCertResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ratpanel/client.go",
    "content": "package ratpanel\n\nimport (\n\t\"bytes\"\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl string, accessTokenId int64, accessToken string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif accessTokenId == 0 {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset accessTokenId\")\n\t}\n\tif accessToken == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset accessToken\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")+\"/api\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\tvar body []byte\n\t\t\tvar err error\n\n\t\t\tif req.Body != nil {\n\t\t\t\tbody, err = io.ReadAll(req.Body)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treq.Body = io.NopCloser(bytes.NewReader(body))\n\t\t\t}\n\n\t\t\tcanonicalPath := req.URL.Path\n\t\t\tif !strings.HasPrefix(canonicalPath, \"/api\") {\n\t\t\t\tindex := strings.Index(canonicalPath, \"/api\")\n\t\t\t\tif index != -1 {\n\t\t\t\t\tcanonicalPath = canonicalPath[index:]\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcanonicalRequest := fmt.Sprintf(\"%s\\n%s\\n%s\\n%s\",\n\t\t\t\treq.Method,\n\t\t\t\tcanonicalPath,\n\t\t\t\treq.URL.Query().Encode(),\n\t\t\t\tsumSha256(string(body)))\n\n\t\t\ttimestamp := time.Now().Unix()\n\t\t\treq.Header.Set(\"X-Timestamp\", fmt.Sprintf(\"%d\", timestamp))\n\n\t\t\tstringToSign := fmt.Sprintf(\"%s\\n%d\\n%s\",\n\t\t\t\t\"HMAC-SHA256\",\n\t\t\t\ttimestamp,\n\t\t\t\tsumSha256(canonicalRequest))\n\t\t\tsignature := sumHmacSha256(stringToSign, accessToken)\n\t\t\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"HMAC-SHA256 Credential=%d, Signature=%s\", accessTokenId, signature))\n\n\t\t\treturn nil\n\t\t})\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tmessage := res.GetMessage(); tmessage != \"success\" {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: message='%s'\", tmessage)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n\nfunc sumSha256(str string) string {\n\tsum := sha256.Sum256([]byte(str))\n\tdst := make([]byte, hex.EncodedLen(len(sum)))\n\thex.Encode(dst, sum[:])\n\treturn string(dst)\n}\n\nfunc sumHmacSha256(data string, secret string) string {\n\th := hmac.New(sha256.New, []byte(secret))\n\th.Write([]byte(data))\n\treturn hex.EncodeToString(h.Sum(nil))\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ratpanel/types.go",
    "content": "package ratpanel\n\ntype sdkResponse interface {\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tMessage *string `json:\"msg,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n"
  },
  {
    "path": "pkg/sdk3rd/safeline/api_update_certificate.go",
    "content": "package safeline\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype UpdateCertificateRequest struct {\n\tId     int64             `json:\"id\"`\n\tType   int32             `json:\"type\"`\n\tManual *CertificateManul `json:\"manual\"`\n}\n\ntype UpdateCertificateResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) UpdateCertificate(req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {\n\treturn c.UpdateCertificateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) UpdateCertificateWithContext(ctx context.Context, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/api/open/cert\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateCertificateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/safeline/client.go",
    "content": "package safeline\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl, apiToken string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\tif apiToken == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset apiToken\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")).\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetHeader(\"X-SLCE-API-TOKEN\", apiToken)\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif terrcode := res.GetErrCode(); terrcode != \"\" {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: err='%s', msg='%s'\", terrcode, res.GetErrMsg())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/safeline/types.go",
    "content": "package safeline\n\ntype sdkResponse interface {\n\tGetErrCode() string\n\tGetErrMsg() string\n}\n\ntype sdkResponseBase struct {\n\tErrCode *string `json:\"err,omitempty\"`\n\tErrMsg  *string `json:\"msg,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetErrCode() string {\n\tif r.ErrCode == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.ErrCode\n}\n\nfunc (r *sdkResponseBase) GetErrMsg() string {\n\tif r.ErrMsg == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.ErrMsg\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype CertificateManul struct {\n\tCrt string `json:\"crt\"`\n\tKey string `json:\"key\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/synologydsm/api_auth_login.go",
    "content": "﻿package synologydsm\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype LoginRequest struct {\n\tAccount  string `json:\"account\"            url:\"account\"`\n\tPassword string `json:\"passwd\"             url:\"passwd\"`\n\tOtpCode  string `json:\"otp_code,omitempty\" url:\"otp_code,omitempty\"`\n}\n\ntype LoginResponse struct {\n\tsdkResponseBase\n\tData *struct {\n\t\tSid       string `json:\"sid\"`\n\t\tSynoToken string `json:\"synotoken\"`\n\t\tDeviceId  string `json:\"device_id,omitempty\"`\n\t\tDid       string `json:\"did,omitempty\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) Login(req *LoginRequest) (*LoginResponse, error) {\n\tconst AUTH_API_NAME = \"SYNO.API.Auth\"\n\tif c.authApiPath == \"\" || c.authApiVersion == 0 {\n\t\tqueryInfoReq := &QueryAPIInfoRequest{\n\t\t\tQuery: AUTH_API_NAME,\n\t\t}\n\t\tqueryInfoResp, err := c.QueryAPIInfo(queryInfoReq)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"sdkerr: failed to query API info: %w\", err)\n\t\t} else {\n\t\t\tauthApiInfo, ok := queryInfoResp.Data[AUTH_API_NAME]\n\t\t\tif !ok {\n\t\t\t\treturn nil, fmt.Errorf(\"sdkerr: failed to query API info: \\\"%s\\\" not found\", AUTH_API_NAME)\n\t\t\t}\n\n\t\t\tc.authApiPath = authApiInfo.Path\n\t\t\tc.authApiVersion = authApiInfo.MaxVersion\n\t\t}\n\t}\n\n\tparams := url.Values{\n\t\t\"api\":                 {AUTH_API_NAME},\n\t\t\"version\":             {strconv.Itoa(c.authApiVersion)},\n\t\t\"method\":              {\"login\"},\n\t\t\"format\":              {\"sid\"},\n\t\t\"enable_syno_token\":   {\"yes\"},\n\t\t\"enable_device_token\": {\"yes\"},\n\t\t\"device_name\":         {\"Certimate\"},\n\t}\n\n\tvalues, err := qs.Values(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor k := range values {\n\t\tparams.Set(k, values.Get(k))\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf(\"/webapi/%s?%s\", c.authApiPath, params.Encode()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &LoginResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\tif result != nil && result.GetErrorCode() > 0 {\n\t\t\terrcode := result.GetErrorCode()\n\t\t\terrdesc := getAuthErrorDescription(errcode)\n\t\t\treturn result, fmt.Errorf(\"sdkerr: code='%d', desc='%s'\", errcode, errdesc)\n\t\t}\n\t\treturn result, err\n\t}\n\n\tif result.Data.Sid == \"\" || result.Data.SynoToken == \"\" {\n\t\treturn result, fmt.Errorf(\"sdkerr: login succeeded but the sid or synotoken is empty\")\n\t}\n\n\tc.synoTokenMtx.Lock()\n\tdefer c.synoTokenMtx.Unlock()\n\tc.sid = result.Data.Sid\n\tc.synoToken = result.Data.SynoToken\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/synologydsm/api_auth_logout.go",
    "content": "﻿package synologydsm\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n)\n\ntype LogoutResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) Logout() (*LogoutResponse, error) {\n\tif c.sid == \"\" {\n\t\tresult := &LogoutResponse{}\n\t\tresult.Success = true\n\t\treturn result, nil\n\t}\n\n\tparams := url.Values{\n\t\t\"api\":     {\"SYNO.API.Auth\"},\n\t\t\"version\": {strconv.Itoa(c.authApiVersion)},\n\t\t\"method\":  {\"logout\"},\n\t\t\"_sid\":    {c.sid},\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf(\"/webapi/%s?%s\", c.authApiPath, params.Encode()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &LogoutResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\tc.synoTokenMtx.Lock()\n\tdefer c.synoTokenMtx.Unlock()\n\tc.sid = \"\"\n\tc.synoToken = \"\"\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/synologydsm/api_core_certificate_crt_list.go",
    "content": "﻿package synologydsm\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype ListCertificatesResponse struct {\n\tsdkResponseBase\n\tData *struct {\n\t\tCertificates []*CertificateInfo `json:\"certificates\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) ListCertificates() (*ListCertificatesResponse, error) {\n\tparams := url.Values{\n\t\t\"api\":     {\"SYNO.Core.Certificate.CRT\"},\n\t\t\"method\":  {\"list\"},\n\t\t\"version\": {\"1\"},\n\t\t\"_sid\":    {c.sid},\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, \"/webapi/entry.cgi\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetHeader(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\thttpreq.SetFormDataFromValues(params)\n\t}\n\n\tresult := &ListCertificatesResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/synologydsm/api_core_certificate_import.go",
    "content": "﻿package synologydsm\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n)\n\ntype ImportCertificateRequest struct {\n\tID          string `json:\"id\"         url:\"id\"`\n\tDescription string `json:\"desc\"       url:\"desc\"`\n\tKey         string `json:\"key\"        url:\"key\"`\n\tCert        string `json:\"cert\"       url:\"cert\"`\n\tInterCert   string `json:\"inter_cert\" url:\"inter_cert\"`\n\tAsDefault   bool   `json:\"as_default\" url:\"as_default\"`\n}\n\ntype ImportCertificateResponse struct {\n\tsdkResponseBase\n\tData *struct {\n\t\tRestartHttpd bool `json:\"restart_httpd\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) ImportCertificate(req *ImportCertificateRequest) (*ImportCertificateResponse, error) {\n\tparams := url.Values{\n\t\t\"api\":       {\"SYNO.Core.Certificate\"},\n\t\t\"method\":    {\"import\"},\n\t\t\"version\":   {\"1\"},\n\t\t\"_sid\":      {c.sid},\n\t\t\"SynoToken\": {c.synoToken},\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf(\"/webapi/entry.cgi?%s\", params.Encode()))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetMultipartField(\"key\", \"key.pem\", \"text/plain\", strings.NewReader(req.Key))\n\t\thttpreq.SetMultipartField(\"cert\", \"cert.pem\", \"text/plain\", strings.NewReader(req.Cert))\n\t\thttpreq.SetMultipartField(\"inter_cert\", \"chain.pem\", \"text/plain\", strings.NewReader(req.InterCert))\n\t\thttpreq.SetMultipartField(\"id\", \"\", \"\", strings.NewReader(req.ID))\n\t\thttpreq.SetMultipartField(\"desc\", \"\", \"\", strings.NewReader(req.Description))\n\t\tif req.AsDefault {\n\t\t\thttpreq.SetMultipartField(\"as_default\", \"\", \"\", strings.NewReader(\"true\"))\n\t\t}\n\t}\n\n\tresult := &ImportCertificateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/synologydsm/api_core_certificate_service_set.go",
    "content": "﻿package synologydsm\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype ServiceCertificateSetting struct {\n\tService   *CertificateService `json:\"service\"`\n\tOldCertID string              `json:\"old_id\"`\n\tCertID    string              `json:\"id\"`\n}\n\ntype SetServiceCertificateRequest struct {\n\tSettings []*ServiceCertificateSetting `json:\"settings\"`\n}\n\ntype SetServiceCertificateResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) SetServiceCertificate(req *SetServiceCertificateRequest) (*SetServiceCertificateResponse, error) {\n\tbsettings, _ := json.Marshal(req.Settings)\n\tparams := url.Values{\n\t\t\"api\":      {\"SYNO.Core.Certificate.Service\"},\n\t\t\"method\":   {\"set\"},\n\t\t\"version\":  {\"1\"},\n\t\t\"settings\": {string(bsettings)},\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf(\"/webapi/entry.cgi?_sid=%s\", c.sid))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetHeader(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\thttpreq.SetFormDataFromValues(params)\n\t}\n\n\tresult := &SetServiceCertificateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/synologydsm/api_info_query.go",
    "content": "﻿package synologydsm\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype QueryAPIInfoRequest struct {\n\tQuery string `json:\"query\" url:\"query\"`\n}\n\ntype QueryAPIInfoResponse struct {\n\tsdkResponseBase\n\tData map[string]APIInfo `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) QueryAPIInfo(req *QueryAPIInfoRequest) (*QueryAPIInfoResponse, error) {\n\tparams := url.Values{\n\t\t\"api\":     {\"SYNO.API.Info\"},\n\t\t\"version\": {\"1\"},\n\t\t\"method\":  {\"query\"},\n\t}\n\n\tvalues, err := qs.Values(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor k := range values {\n\t\tparams.Set(k, values.Get(k))\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf(\"/webapi/query.cgi?%s\", params.Encode()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &QueryAPIInfoResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/synologydsm/client.go",
    "content": "package synologydsm\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tauthApiPath    string\n\tauthApiVersion int\n\n\tsid          string\n\tsynoToken    string\n\tsynoTokenMtx sync.Mutex\n\n\tclient *resty.Client\n}\n\nfunc NewClient(serverUrl string) (*Client, error) {\n\tif serverUrl == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset serverUrl\")\n\t}\n\tif _, err := url.Parse(serverUrl); err != nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: invalid serverUrl: %w\", err)\n\t}\n\n\tclient := &Client{}\n\tclient.client = resty.New().\n\t\tSetBaseURL(strings.TrimRight(serverUrl, \"/\")).\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\tif client.synoToken != \"\" {\n\t\t\t\treq.Header.Set(\"X-SYNO-TOKEN\", client.synoToken)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\treturn client, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.client.SetTLSClientConfig(config)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t} else {\n\t\tif tsuccess := res.GetSuccess(); !tsuccess {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: code='%d'\", res.GetErrorCode())\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/synologydsm/types.go",
    "content": "package synologydsm\n\ntype sdkResponse interface {\n\tGetSuccess() bool\n\tGetErrorCode() int\n}\n\ntype sdkResponseBase struct {\n\tSuccess bool      `json:\"success\"`\n\tError   *APIError `json:\"error,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetSuccess() bool {\n\treturn r.Success\n}\n\nfunc (r *sdkResponseBase) GetErrorCode() int {\n\tif r.Error == nil {\n\t\tif r.Success {\n\t\t\treturn 0\n\t\t}\n\t\treturn -1\n\t}\n\n\treturn r.Error.Code\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype APIError struct {\n\tCode int `json:\"code,omitempty\"`\n}\n\ntype APIInfo struct {\n\tPath       string `json:\"path\"`\n\tMinVersion int    `json:\"minVersion\"`\n\tMaxVersion int    `json:\"maxVersion\"`\n}\n\ntype CertificateInfo struct {\n\tID          string `json:\"id\"`\n\tDescription string `json:\"desc\"`\n\tIsDefault   bool   `json:\"is_default\"`\n\tIsBroken    bool   `json:\"is_broken\"`\n\tIssuer      struct {\n\t\tCommonName   string `json:\"common_name\"`\n\t\tCountry      string `json:\"country\"`\n\t\tOrganization string `json:\"organization\"`\n\t} `json:\"issuer\"`\n\tSubject struct {\n\t\tCommonName   string   `json:\"common_name\"`\n\t\tCountry      string   `json:\"country\"`\n\t\tOrganization string   `json:\"organization\"`\n\t\tSAN          []string `json:\"sub_alt_name\"`\n\t} `json:\"subject\"`\n\tValidFrom          string                `json:\"valid_from\"`\n\tValidTill          string                `json:\"valid_till\"`\n\tSignatureAlgorithm string                `json:\"signature_algorithm\"`\n\tRenewable          bool                  `json:\"renewable\"`\n\tServices           []*CertificateService `json:\"services\"`\n}\n\ntype CertificateService struct {\n\tDisplayName     string `json:\"display_name\"`\n\tDisplayNameI18N string `json:\"display_name_i18n,omitempty\"`\n\tIsPkg           bool   `json:\"isPkg\"`\n\tOwner           string `json:\"owner\"`\n\tService         string `json:\"service\"`\n\tSubscriber      string `json:\"subscriber\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/synologydsm/utils.go",
    "content": "﻿package synologydsm\n\nfunc getAuthErrorDescription(code int) string {\n\tswitch code {\n\tcase 100:\n\t\treturn \"Unknown error\"\n\tcase 101:\n\t\treturn \"Invalid parameters\"\n\tcase 102:\n\t\treturn \"API does not exist\"\n\tcase 103:\n\t\treturn \"Method does not exist\"\n\tcase 104:\n\t\treturn \"This API version is not supported\"\n\tcase 105:\n\t\treturn \"Insufficient user privilege\"\n\tcase 106:\n\t\treturn \"Connection time out\"\n\tcase 107:\n\t\treturn \"Multiple login detected\"\n\tcase 400:\n\t\treturn \"Invalid password or account does not exist\"\n\tcase 401:\n\t\treturn \"Guest or disabled account\"\n\tcase 402:\n\t\treturn \"Permission denied\"\n\tcase 403:\n\t\treturn \"2-factor authentication code required (OTP)\"\n\tcase 404:\n\t\treturn \"Failed to authenticate 2-factor authentication code\"\n\tcase 405:\n\t\treturn \"Server version is too low or not supported\"\n\tcase 406:\n\t\treturn \"2-factor authentication code expired\"\n\tcase 407:\n\t\treturn \"Login failed: IP has been blocked\"\n\tcase 408:\n\t\treturn \"Expired password\"\n\tcase 409:\n\t\treturn \"Password must be changed (password policy)\"\n\tcase 410:\n\t\treturn \"Account locked (too many failed login attempts)\"\n\tdefault:\n\t\treturn \"Unknown authentication error\"\n\t}\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ucdn/api_get_ucdn_domain_config.go",
    "content": "package ucdn\n\nimport (\n\tucloudcdn \"github.com/ucloud/ucloud-sdk-go/services/ucdn\"\n)\n\ntype GetUcdnDomainConfigRequest = ucloudcdn.GetUcdnDomainConfigRequest\n\ntype GetUcdnDomainConfigResponse = ucloudcdn.GetUcdnDomainConfigResponse\n\nfunc (c *UCDNClient) NewGetUcdnDomainConfigRequest() *GetUcdnDomainConfigRequest {\n\treq := &GetUcdnDomainConfigRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *UCDNClient) GetUcdnDomainConfig(req *GetUcdnDomainConfigRequest) (*GetUcdnDomainConfigResponse, error) {\n\tvar err error\n\tvar res GetUcdnDomainConfigResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"GetUcdnDomainConfig\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ucdn/api_update_ucdn_domain_https_config_v2.go",
    "content": "package ucdn\n\nimport (\n\tucloudcdn \"github.com/ucloud/ucloud-sdk-go/services/ucdn\"\n)\n\ntype UpdateUcdnDomainHttpsConfigV2Request = ucloudcdn.UpdateUcdnDomainHttpsConfigV2Request\n\ntype UpdateUcdnDomainHttpsConfigV2Response = ucloudcdn.UpdateUcdnDomainHttpsConfigV2Response\n\nfunc (c *UCDNClient) NewUpdateUcdnDomainHttpsConfigV2Request() *UpdateUcdnDomainHttpsConfigV2Request {\n\treq := &UpdateUcdnDomainHttpsConfigV2Request{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *UCDNClient) UpdateUcdnDomainHttpsConfigV2(req *UpdateUcdnDomainHttpsConfigV2Request) (*UpdateUcdnDomainHttpsConfigV2Response, error) {\n\tvar err error\n\tvar res UpdateUcdnDomainHttpsConfigV2Response\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"UpdateUcdnDomainHttpsConfigV2\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ucdn/client.go",
    "content": "package ucdn\n\nimport (\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n)\n\ntype UCDNClient struct {\n\t*ucloud.Client\n}\n\nfunc NewClient(config *ucloud.Config, credential *auth.Credential) *UCDNClient {\n\tmeta := ucloud.ClientMeta{Product: \"UCDN\"}\n\tclient := ucloud.NewClientWithMeta(config, credential, meta)\n\treturn &UCDNClient{\n\t\tclient,\n\t}\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ucdn/types.go",
    "content": "package ucdn\n\nimport (\n\tucloudcdn \"github.com/ucloud/ucloud-sdk-go/services/ucdn\"\n)\n\ntype DomainConfigInfo = ucloudcdn.DomainConfigInfo\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/udnr/api_add_domain_dns.go",
    "content": "package udnr\n\nimport (\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/request\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/response\"\n)\n\ntype AddDomainDNSRequest struct {\n\trequest.CommonBase\n\n\tDn         *string `required:\"true\"`\n\tDnsType    *string `required:\"true\"`\n\tRecordName *string `required:\"true\"`\n\tContent    *string `required:\"true\"`\n\tTTL        *string `required:\"true\"`\n\tPrio       *string `required:\"false\"`\n}\n\ntype AddDomainDNSResponse struct {\n\tresponse.CommonBase\n}\n\nfunc (c *UDNRClient) NewAddDomainDNSRequest() *AddDomainDNSRequest {\n\treq := &AddDomainDNSRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(false)\n\treturn req\n}\n\nfunc (c *UDNRClient) AddDomainDNS(req *AddDomainDNSRequest) (*AddDomainDNSResponse, error) {\n\tvar err error\n\tvar res AddDomainDNSResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"UdnrDomainDNSAdd\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/udnr/api_delete_domain_dns.go",
    "content": "package udnr\n\nimport (\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/request\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/response\"\n)\n\ntype DeleteDomainDNSRequest struct {\n\trequest.CommonBase\n\n\tDn         *string `required:\"true\"`\n\tDnsType    *string `required:\"true\"`\n\tRecordName *string `required:\"true\"`\n\tContent    *string `required:\"true\"`\n}\n\ntype DeleteDomainDNSResponse struct {\n\tresponse.CommonBase\n}\n\nfunc (c *UDNRClient) NewDeleteDomainDNSRequest() *DeleteDomainDNSRequest {\n\treq := &DeleteDomainDNSRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *UDNRClient) DeleteDomainDNS(req *DeleteDomainDNSRequest) (*DeleteDomainDNSResponse, error) {\n\tvar err error\n\tvar res DeleteDomainDNSResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"UdnrDeleteDnsRecord\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/udnr/api_query_domain_dns.go",
    "content": "package udnr\n\nimport (\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/request\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/response\"\n)\n\ntype QueryDomainDNSRequest struct {\n\trequest.CommonBase\n\n\tDn *string `required:\"true\"`\n}\n\ntype QueryDomainDNSResponse struct {\n\tresponse.CommonBase\n\n\tData []DomainDNSRecord\n}\n\nfunc (c *UDNRClient) NewQueryDomainDNSRequest() *QueryDomainDNSRequest {\n\treq := &QueryDomainDNSRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *UDNRClient) QueryDomainDNS(req *QueryDomainDNSRequest) (*QueryDomainDNSResponse, error) {\n\tvar err error\n\tvar res QueryDomainDNSResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"UdnrDomainDNSQuery\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/udnr/client.go",
    "content": "package udnr\n\nimport (\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n)\n\ntype UDNRClient struct {\n\t*ucloud.Client\n}\n\nfunc NewClient(config *ucloud.Config, credential *auth.Credential) *UDNRClient {\n\tmeta := ucloud.ClientMeta{Product: \"UDNR\"}\n\tclient := ucloud.NewClientWithMeta(config, credential, meta)\n\treturn &UDNRClient{\n\t\tclient,\n\t}\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/udnr/types.go",
    "content": "package udnr\n\ntype DomainDNSRecord struct {\n\tDnsType    string\n\tRecordName string\n\tContent    string\n\tTTL        string\n\tPrio       string\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/uewaf/api_add_waf_domain_certificate_info.go",
    "content": "package uewaf\n\nimport (\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/request\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/response\"\n)\n\ntype AddWafDomainCertificateInfoRequest struct {\n\trequest.CommonBase\n\n\tDomain          *string `required:\"true\"`\n\tCertificateName *string `required:\"true\"`\n\tSslPublicKey    *string `required:\"true\"`\n\tSslPrivateKey   *string `required:\"false\"`\n\tSslMD           *string `required:\"false\"`\n\tSslKeyLess      *string `required:\"false\"`\n}\n\ntype AddWafDomainCertificateInfoResponse struct {\n\tresponse.CommonBase\n\n\tId int\n}\n\nfunc (c *UEWAFClient) NewAddWafDomainCertificateInfoRequest() *AddWafDomainCertificateInfoRequest {\n\treq := &AddWafDomainCertificateInfoRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *UEWAFClient) AddWafDomainCertificateInfo(req *AddWafDomainCertificateInfoRequest) (*AddWafDomainCertificateInfoResponse, error) {\n\tvar err error\n\tvar res AddWafDomainCertificateInfoResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"AddWafDomainCertificateInfo\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/uewaf/client.go",
    "content": "package uewaf\n\nimport (\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n)\n\ntype UEWAFClient struct {\n\t*ucloud.Client\n}\n\nfunc NewClient(config *ucloud.Config, credential *auth.Credential) *UEWAFClient {\n\tmeta := ucloud.ClientMeta{Product: \"UEWAF\"}\n\tclient := ucloud.NewClientWithMeta(config, credential, meta)\n\treturn &UEWAFClient{\n\t\tclient,\n\t}\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ufile/api_add_ufile_ssl_cert.go",
    "content": "package ufile\n\nimport (\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/request\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/response\"\n)\n\ntype AddUFileSSLCertRequest struct {\n\trequest.CommonBase\n\n\tBucketName      *string `required:\"true\"`\n\tDomain          *string `required:\"true\"`\n\tCertificateName *string `required:\"true\"`\n\tUSSLId          *string `required:\"false\"`\n}\n\ntype AddUFileSSLCertResponse struct {\n\tresponse.CommonBase\n}\n\nfunc (c *UFileClient) NewAddUFileSSLCertRequest() *AddUFileSSLCertRequest {\n\treq := &AddUFileSSLCertRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *UFileClient) AddUFileSSLCert(req *AddUFileSSLCertRequest) (*AddUFileSSLCertResponse, error) {\n\tvar err error\n\tvar res AddUFileSSLCertResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"AddUFileSSLCert\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ufile/client.go",
    "content": "package ufile\n\nimport (\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n)\n\ntype UFileClient struct {\n\t*ucloud.Client\n}\n\nfunc NewClient(config *ucloud.Config, credential *auth.Credential) *UFileClient {\n\tmeta := ucloud.ClientMeta{Product: \"UFile\"}\n\tclient := ucloud.NewClientWithMeta(config, credential, meta)\n\treturn &UFileClient{\n\t\tclient,\n\t}\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ulb/api_add_ssl_binding.go",
    "content": "package ulb\n\nimport (\n\tucloudlb \"github.com/ucloud/ucloud-sdk-go/services/ulb\"\n)\n\ntype AddSSLBindingRequest = ucloudlb.AddSSLBindingRequest\n\ntype AddSSLBindingResponse = ucloudlb.AddSSLBindingResponse\n\nfunc (c *ULBClient) NewAddSSLBindingRequest() *AddSSLBindingRequest {\n\treq := &AddSSLBindingRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *ULBClient) AddSSLBinding(req *AddSSLBindingRequest) (*AddSSLBindingResponse, error) {\n\tvar err error\n\tvar res AddSSLBindingResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"AddSSLBinding\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ulb/api_bind_ssl.go",
    "content": "package ulb\n\nimport (\n\tucloudlb \"github.com/ucloud/ucloud-sdk-go/services/ulb\"\n)\n\ntype BindSSLRequest = ucloudlb.BindSSLRequest\n\ntype BindSSLResponse = ucloudlb.BindSSLResponse\n\nfunc (c *ULBClient) NewBindSSLRequest() *BindSSLRequest {\n\treq := &BindSSLRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *ULBClient) BindSSL(req *BindSSLRequest) (*BindSSLResponse, error) {\n\tvar err error\n\tvar res BindSSLResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"BindSSL\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ulb/api_create_ssl.go",
    "content": "package ulb\n\nimport (\n\tucloudlb \"github.com/ucloud/ucloud-sdk-go/services/ulb\"\n)\n\ntype CreateSSLRequest = ucloudlb.CreateSSLRequest\n\ntype CreateSSLResponse = ucloudlb.CreateSSLResponse\n\nfunc (c *ULBClient) NewCreateSSLRequest() *CreateSSLRequest {\n\treq := &CreateSSLRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *ULBClient) CreateSSL(req *CreateSSLRequest) (*CreateSSLResponse, error) {\n\tvar err error\n\tvar res CreateSSLResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"CreateSSL\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ulb/api_delete_ssl_binding.go",
    "content": "package ulb\n\nimport (\n\tucloudlb \"github.com/ucloud/ucloud-sdk-go/services/ulb\"\n)\n\ntype DeleteSSLBindingRequest = ucloudlb.DeleteSSLBindingRequest\n\ntype DeleteSSLBindingResponse = ucloudlb.DeleteSSLBindingResponse\n\nfunc (c *ULBClient) NewDeleteSSLBindingRequest() *DeleteSSLBindingRequest {\n\treq := &DeleteSSLBindingRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *ULBClient) DeleteSSLBinding(req *DeleteSSLBindingRequest) (*DeleteSSLBindingResponse, error) {\n\tvar err error\n\tvar res DeleteSSLBindingResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"DeleteSSLBinding\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ulb/api_describe_listeners.go",
    "content": "package ulb\n\nimport (\n\tucloudlb \"github.com/ucloud/ucloud-sdk-go/services/ulb\"\n)\n\ntype DescribeListenersRequest = ucloudlb.DescribeListenersRequest\n\ntype DescribeListenersResponse = ucloudlb.DescribeListenersResponse\n\nfunc (c *ULBClient) NewDescribeListenersRequest() *DescribeListenersRequest {\n\treq := &DescribeListenersRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *ULBClient) DescribeListeners(req *DescribeListenersRequest) (*DescribeListenersResponse, error) {\n\tvar err error\n\tvar res DescribeListenersResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"DescribeListeners\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ulb/api_describe_ssl.go",
    "content": "package ulb\n\nimport (\n\tucloudlb \"github.com/ucloud/ucloud-sdk-go/services/ulb\"\n)\n\ntype DescribeSSLRequest = ucloudlb.DescribeSSLRequest\n\ntype DescribeSSLResponse = ucloudlb.DescribeSSLResponse\n\nfunc (c *ULBClient) NewDescribeSSLRequest() *DescribeSSLRequest {\n\treq := &DescribeSSLRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *ULBClient) DescribeSSL(req *DescribeSSLRequest) (*DescribeSSLResponse, error) {\n\tvar err error\n\tvar res DescribeSSLResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"DescribeSSL\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ulb/api_describe_ssl_v2.go",
    "content": "package ulb\n\nimport (\n\tucloudlb \"github.com/ucloud/ucloud-sdk-go/services/ulb\"\n)\n\ntype DescribeSSLV2Request = ucloudlb.DescribeSSLV2Request\n\ntype DescribeSSLV2Response = ucloudlb.DescribeSSLV2Response\n\nfunc (c *ULBClient) NewDescribeSSLV2Request() *DescribeSSLV2Request {\n\treq := &DescribeSSLV2Request{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *ULBClient) DescribeSSLV2(req *DescribeSSLV2Request) (*DescribeSSLV2Response, error) {\n\tvar err error\n\tvar res DescribeSSLV2Response\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"DescribeSSLV2\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ulb/api_describe_vserver.go",
    "content": "package ulb\n\nimport (\n\tucloudlb \"github.com/ucloud/ucloud-sdk-go/services/ulb\"\n)\n\ntype DescribeVServerRequest = ucloudlb.DescribeVServerRequest\n\ntype DescribeVServerResponse = ucloudlb.DescribeVServerResponse\n\nfunc (c *ULBClient) NewDescribeVServerRequest() *DescribeVServerRequest {\n\treq := &DescribeVServerRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *ULBClient) DescribeVServer(req *DescribeVServerRequest) (*DescribeVServerResponse, error) {\n\tvar err error\n\tvar res DescribeVServerResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"DescribeVServer\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ulb/api_unbind_ssl.go",
    "content": "package ulb\n\nimport (\n\tucloudlb \"github.com/ucloud/ucloud-sdk-go/services/ulb\"\n)\n\ntype UnbindSSLRequest = ucloudlb.UnbindSSLRequest\n\ntype UnbindSSLResponse = ucloudlb.UnbindSSLResponse\n\nfunc (c *ULBClient) NewUnbindSSLRequest() *UnbindSSLRequest {\n\treq := &UnbindSSLRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *ULBClient) UnbindSSL(req *UnbindSSLRequest) (*UnbindSSLResponse, error) {\n\tvar err error\n\tvar res UnbindSSLResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"UnbindSSL\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ulb/api_update_listener_attribute.go",
    "content": "package ulb\n\nimport (\n\tucloudlb \"github.com/ucloud/ucloud-sdk-go/services/ulb\"\n)\n\ntype UpdateListenerAttributeRequest = ucloudlb.UpdateListenerAttributeRequest\n\ntype UpdateListenerAttributeResponse = ucloudlb.UpdateListenerAttributeResponse\n\nfunc (c *ULBClient) NewUpdateListenerAttributeRequest() *UpdateListenerAttributeRequest {\n\treq := &UpdateListenerAttributeRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *ULBClient) UpdateListenerAttribute(req *UpdateListenerAttributeRequest) (*UpdateListenerAttributeResponse, error) {\n\tvar err error\n\tvar res UpdateListenerAttributeResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"UpdateListenerAttribute\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ulb/client.go",
    "content": "package ulb\n\nimport (\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n)\n\ntype ULBClient struct {\n\t*ucloud.Client\n}\n\nfunc NewClient(config *ucloud.Config, credential *auth.Credential) *ULBClient {\n\tmeta := ucloud.ClientMeta{Product: \"ULB\"}\n\tclient := ucloud.NewClientWithMeta(config, credential, meta)\n\treturn &ULBClient{\n\t\tclient,\n\t}\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/upathx/api_bind_pathx_ssl.go",
    "content": "package upathx\n\nimport (\n\tucloudpathx \"github.com/ucloud/ucloud-sdk-go/services/pathx\"\n)\n\ntype BindPathXSSLRequest = ucloudpathx.BindPathXSSLRequest\n\ntype BindPathXSSLResponse = ucloudpathx.BindPathXSSLResponse\n\nfunc (c *UPathXClient) NewBindPathXSSLRequest() *BindPathXSSLRequest {\n\treq := &BindPathXSSLRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *UPathXClient) BindPathXSSL(req *BindPathXSSLRequest) (*BindPathXSSLResponse, error) {\n\tvar err error\n\tvar res BindPathXSSLResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"BindPathXSSL\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/upathx/api_create_pathx_ssl.go",
    "content": "package upathx\n\nimport (\n\tucloudpathx \"github.com/ucloud/ucloud-sdk-go/services/pathx\"\n)\n\ntype CreatePathXSSLRequest = ucloudpathx.CreatePathXSSLRequest\n\ntype CreatePathXSSLResponse = ucloudpathx.CreatePathXSSLResponse\n\nfunc (c *UPathXClient) NewCreatePathXSSLRequest() *CreatePathXSSLRequest {\n\treq := &CreatePathXSSLRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *UPathXClient) CreatePathXSSL(req *CreatePathXSSLRequest) (*CreatePathXSSLResponse, error) {\n\tvar err error\n\tvar res CreatePathXSSLResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"CreatePathXSSL\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/upathx/api_describe_pathx_ssl.go",
    "content": "package upathx\n\nimport (\n\tucloudpathx \"github.com/ucloud/ucloud-sdk-go/services/pathx\"\n)\n\ntype DescribePathXSSLRequest = ucloudpathx.DescribePathXSSLRequest\n\ntype DescribePathXSSLResponse = ucloudpathx.DescribePathXSSLResponse\n\nfunc (c *UPathXClient) NewDescribePathXSSLRequest() *DescribePathXSSLRequest {\n\treq := &DescribePathXSSLRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *UPathXClient) DescribePathXSSL(req *DescribePathXSSLRequest) (*DescribePathXSSLResponse, error) {\n\tvar err error\n\tvar res DescribePathXSSLResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"DescribePathXSSL\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/upathx/api_unbind_pathx_ssl.go",
    "content": "package upathx\n\nimport (\n\tucloudpathx \"github.com/ucloud/ucloud-sdk-go/services/pathx\"\n)\n\ntype UnbindPathXSSLRequest = ucloudpathx.UnBindPathXSSLRequest\n\ntype UnbindPathXSSLResponse = ucloudpathx.UnBindPathXSSLResponse\n\nfunc (c *UPathXClient) NewUnbindPathXSSLRequest() *UnbindPathXSSLRequest {\n\treq := &UnbindPathXSSLRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *UPathXClient) UnbindPathXSSL(req *UnbindPathXSSLRequest) (*UnbindPathXSSLResponse, error) {\n\tvar err error\n\tvar res UnbindPathXSSLResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"UnBindPathXSSL\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/upathx/client.go",
    "content": "package upathx\n\nimport (\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n)\n\ntype UPathXClient struct {\n\t*ucloud.Client\n}\n\nfunc NewClient(config *ucloud.Config, credential *auth.Credential) *UPathXClient {\n\tmeta := ucloud.ClientMeta{Product: \"PathX\"}\n\tclient := ucloud.NewClientWithMeta(config, credential, meta)\n\treturn &UPathXClient{\n\t\tclient,\n\t}\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ussl/api_download_certificate.go",
    "content": "package ussl\n\nimport (\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/request\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/response\"\n)\n\ntype DownloadCertificateRequest struct {\n\trequest.CommonBase\n\n\tCertificateID *int `required:\"true\"`\n}\n\ntype DownloadCertificateResponse struct {\n\tresponse.CommonBase\n\n\tCertificateUrl string\n\tCertCA         *CertificateDownloadInfo\n\tCertificate    *CertificateDownloadInfo\n}\n\nfunc (c *USSLClient) NewDownloadCertificateRequest() *DownloadCertificateRequest {\n\treq := &DownloadCertificateRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *USSLClient) DownloadCertificate(req *DownloadCertificateRequest) (*DownloadCertificateResponse, error) {\n\tvar err error\n\tvar res DownloadCertificateResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"DownloadCertificate\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ussl/api_get_certificate_detail_info.go",
    "content": "package ussl\n\nimport (\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/request\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/response\"\n)\n\ntype GetCertificateDetailInfoRequest struct {\n\trequest.CommonBase\n\n\tCertificateID *int `required:\"true\"`\n}\n\ntype GetCertificateDetailInfoResponse struct {\n\tresponse.CommonBase\n\n\tCertificateInfo *CertificateInfo\n}\n\nfunc (c *USSLClient) NewGetCertificateDetailInfoRequest() *GetCertificateDetailInfoRequest {\n\treq := &GetCertificateDetailInfoRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *USSLClient) GetCertificateDetailInfo(req *GetCertificateDetailInfoRequest) (*GetCertificateDetailInfoResponse, error) {\n\tvar err error\n\tvar res GetCertificateDetailInfoResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"GetCertificateDetailInfo\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ussl/api_get_certificate_list.go",
    "content": "package ussl\n\nimport (\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/request\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/response\"\n)\n\ntype GetCertificateListRequest struct {\n\trequest.CommonBase\n\n\tMode           *string `required:\"true\"`\n\tStateCode      *string `required:\"false\"`\n\tBrand          *string `required:\"false\"`\n\tCaOrganization *string `required:\"false\"`\n\tDomain         *string `required:\"false\"`\n\tSort           *string `required:\"false\"`\n\tPage           *int    `required:\"false\"`\n\tPageSize       *int    `required:\"false\"`\n}\n\ntype GetCertificateListResponse struct {\n\tresponse.CommonBase\n\n\tCertificateList []*CertificateListItem\n\tTotalCount      int\n}\n\nfunc (c *USSLClient) NewGetCertificateListRequest() *GetCertificateListRequest {\n\treq := &GetCertificateListRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(true)\n\treturn req\n}\n\nfunc (c *USSLClient) GetCertificateList(req *GetCertificateListRequest) (*GetCertificateListResponse, error) {\n\tvar err error\n\tvar res GetCertificateListResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"GetCertificateList\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ussl/api_upload_normal_certificate.go",
    "content": "package ussl\n\nimport (\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/request\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/response\"\n)\n\ntype UploadNormalCertificateRequest struct {\n\trequest.CommonBase\n\n\tCertificateName *string `required:\"true\"`\n\tSslPublicKey    *string `required:\"true\"`\n\tSslPrivateKey   *string `required:\"true\"`\n\tSslMD5          *string `required:\"true\"`\n\tSslCaKey        *string `required:\"false\"`\n}\n\ntype UploadNormalCertificateResponse struct {\n\tresponse.CommonBase\n\n\tCertificateID  int\n\tLongResourceID string\n}\n\nfunc (c *USSLClient) NewUploadNormalCertificateRequest() *UploadNormalCertificateRequest {\n\treq := &UploadNormalCertificateRequest{}\n\n\tc.Client.SetupRequest(req)\n\n\treq.SetRetryable(false)\n\treturn req\n}\n\nfunc (c *USSLClient) UploadNormalCertificate(req *UploadNormalCertificateRequest) (*UploadNormalCertificateResponse, error) {\n\tvar err error\n\tvar res UploadNormalCertificateResponse\n\n\treqCopier := *req\n\n\terr = c.Client.InvokeAction(\"UploadNormalCertificate\", &reqCopier, &res)\n\tif err != nil {\n\t\treturn &res, err\n\t}\n\n\treturn &res, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ussl/client.go",
    "content": "package ussl\n\nimport (\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud\"\n\t\"github.com/ucloud/ucloud-sdk-go/ucloud/auth\"\n)\n\ntype USSLClient struct {\n\t*ucloud.Client\n}\n\nfunc NewClient(config *ucloud.Config, credential *auth.Credential) *USSLClient {\n\tmeta := ucloud.ClientMeta{Product: \"USSL\"}\n\tclient := ucloud.NewClientWithMeta(config, credential, meta)\n\treturn &USSLClient{\n\t\tclient,\n\t}\n}\n"
  },
  {
    "path": "pkg/sdk3rd/ucloud/ussl/types.go",
    "content": "package ussl\n\ntype CertificateListItem struct {\n\tCertificateID     int\n\tCertificateSN     string\n\tCertificateCat    string\n\tMode              string\n\tDomains           string\n\tBrand             string\n\tValidityPeriod    int\n\tType              string\n\tNotBefore         int\n\tNotAfter          int\n\tAlarmState        int\n\tState             string\n\tStateCode         string\n\tName              string\n\tMaxDomainsCount   int\n\tDomainsCount      int\n\tCaChannel         string\n\tCSRAlgorithms     []CSRAlgorithmInfo\n\tTopOrganizationID int\n\tOrganizationID    int\n\tIsFree            int\n\tYearOfValidity    int\n\tChannel           int\n\tCreateTime        int\n\tCertificateUrl    string\n}\n\ntype CSRAlgorithmInfo struct {\n\tAlgorithm       string\n\tAlgorithmOption []string\n}\n\ntype CertificateInfo struct {\n\tType            string\n\tCertificateID   int\n\tCertificateType string\n\tCaOrganization  string\n\tAlgorithm       string\n\tValidityPeriod  int\n\tState           string\n\tStateCode       string\n\tName            string\n\tBrand           string\n\tDomains         string\n\tDomainsCount    int\n\tMode            string\n\tCSROnline       int\n\tCSR             string\n\tCSRKeyParameter string\n\tCSREncryptAlgo  string\n\tIssuedDate      int\n\tExpiredDate     int\n}\n\ntype CertificateDownloadInfo struct {\n\tFileData string\n\tFileName string\n}\n"
  },
  {
    "path": "pkg/sdk3rd/upyun/console/api_get_buckets.go",
    "content": "package console\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tqs \"github.com/google/go-querystring/query\"\n)\n\ntype GetBucketsRequest struct {\n\tBucketName    string `json:\"status\"        url:\"bucket_name\"`\n\tBusinessType  string `json:\"business_type\" url:\"business_type\"`\n\tType          string `json:\"type\"          url:\"type\"`\n\tStatus        string `json:\"state\"         url:\"state\"`\n\tTag           string `json:\"tag\"           url:\"tag\"`\n\tIsSecurityCDN bool   `json:\"security_cdn\"  url:\"security_cdn\"`\n\tWithDomains   bool   `json:\"with_domains\"  url:\"with_domains\"`\n\tPage          int32  `json:\"page\"          url:\"page\"`\n\tPerPage       int32  `json:\"perPage\"       url:\"perPage\"`\n}\n\ntype GetBucketsResponse struct {\n\tsdkResponseBase\n\tData *struct {\n\t\tsdkResponseBaseData\n\t\tBuckets []*BucketInfo `json:\"buckets\"`\n\t\tPager   BucketPager   `json:\"pager\"`\n\t} `json:\"data,omitempty\"`\n}\n\ntype BucketInfo struct {\n\tBucketName    string          `json:\"bucket_name\"`\n\tBusinessType  string          `json:\"business_type\"`\n\tType          string          `json:\"type\"`\n\tStatus        string          `json:\"status\"`\n\tTag           string          `json:\"tag\"`\n\tIsFusionCDN   bool            `json:\"fusion_cdn\"`\n\tIsSecurityCDN bool            `json:\"security_cdn\"`\n\tDomains       []*BucketDomain `json:\"domains\"`\n\tVisible       bool            `json:\"visible\"`\n\tCreatedAt     string          `json:\"created_at\"`\n}\n\ntype BucketDomain struct {\n\tDomain string `json:\"domain\"`\n\tStatus string `json:\"status\"`\n}\n\ntype BucketPager struct {\n\tPage  int32 `json:\"page\"`\n\tPages int64 `json:\"pages\"`\n\tTotal int64 `json:\"total\"`\n}\n\nfunc (c *Client) GetBuckets(req *GetBucketsRequest) (*GetBucketsResponse, error) {\n\treturn c.GetBucketsWithContext(context.Background(), req)\n}\n\nfunc (c *Client) GetBucketsWithContext(ctx context.Context, req *GetBucketsRequest) (*GetBucketsResponse, error) {\n\tif err := c.ensureCookieExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, \"/api/v2/buckets\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\tvalues, err := qs.Values(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\thttpreq.SetQueryParamsFromValues(values)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &GetBucketsResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/upyun/console/api_get_https_certificate_manager.go",
    "content": "package console\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype HttpsCertificateManagerDomain struct {\n\tName       string `json:\"name\"`\n\tType       string `json:\"type\"`\n\tBucketId   int64  `json:\"bucket_id\"`\n\tBucketName string `json:\"bucket_name\"`\n}\n\ntype GetHttpsCertificateManagerResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tsdkResponseBaseData\n\n\t\tAuthenticateNum     int32                           `json:\"authenticate_num\"`\n\t\tAuthenticateDomains []string                        `json:\"authenticate_domain\"`\n\t\tDomains             []HttpsCertificateManagerDomain `json:\"domains\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) GetHttpsCertificateManager(certificateId string) (*GetHttpsCertificateManagerResponse, error) {\n\treturn c.GetHttpsCertificateManagerWithContext(context.Background(), certificateId)\n}\n\nfunc (c *Client) GetHttpsCertificateManagerWithContext(ctx context.Context, certificateId string) (*GetHttpsCertificateManagerResponse, error) {\n\tif certificateId == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset certificateId\")\n\t}\n\n\tif err := c.ensureCookieExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, \"/api/https/certificate/manager/\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetQueryParam(\"certificate_id\", certificateId)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &GetHttpsCertificateManagerResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/upyun/console/api_get_https_service_manager.go",
    "content": "package console\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype GetHttpsServiceManagerResponse struct {\n\tsdkResponseBase\n\tData *struct {\n\t\tsdkResponseBaseData\n\t\tStatus  int32                       `json:\"status\"`\n\t\tDomains []HttpsServiceManagerDomain `json:\"result\"`\n\t} `json:\"data,omitempty\"`\n}\n\ntype HttpsServiceManagerDomain struct {\n\tCertificateId string                            `json:\"certificate_id\"`\n\tCommonName    string                            `json:\"commonName\"`\n\tHttps         bool                              `json:\"https\"`\n\tForceHttps    bool                              `json:\"force_https\"`\n\tPaymentType   string                            `json:\"payment_type\"`\n\tDomainType    string                            `json:\"domain_type\"`\n\tValidity      HttpsServiceManagerDomainValidity `json:\"validity\"`\n}\n\ntype HttpsServiceManagerDomainValidity struct {\n\tStart int64 `json:\"start\"`\n\tEnd   int64 `json:\"end\"`\n}\n\nfunc (c *Client) GetHttpsServiceManager(domain string) (*GetHttpsServiceManagerResponse, error) {\n\treturn c.GetHttpsServiceManagerWithContext(context.Background(), domain)\n}\n\nfunc (c *Client) GetHttpsServiceManagerWithContext(ctx context.Context, domain string) (*GetHttpsServiceManagerResponse, error) {\n\tif domain == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset domain\")\n\t}\n\n\tif err := c.ensureCookieExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, \"/api/https/services/manager\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetQueryParam(\"domain\", domain)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &GetHttpsServiceManagerResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/upyun/console/api_migrate_https_domain.go",
    "content": "package console\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype MigrateHttpsDomainRequest struct {\n\tCertificateId string `json:\"crt_id\"`\n\tDomain        string `json:\"domain_name\"`\n}\n\ntype MigrateHttpsDomainResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tsdkResponseBaseData\n\n\t\tStatus bool `json:\"status\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) MigrateHttpsDomain(req *MigrateHttpsDomainRequest) (*MigrateHttpsDomainResponse, error) {\n\treturn c.MigrateHttpsDomainWithContext(context.Background(), req)\n}\n\nfunc (c *Client) MigrateHttpsDomainWithContext(ctx context.Context, req *MigrateHttpsDomainRequest) (*MigrateHttpsDomainResponse, error) {\n\tif err := c.ensureCookieExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, \"/api/https/migrate/domain\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &MigrateHttpsDomainResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/upyun/console/api_update_https_certificate_manager.go",
    "content": "package console\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype UpdateHttpsCertificateManagerRequest struct {\n\tCertificateId string `json:\"certificate_id\"`\n\tDomain        string `json:\"domain\"`\n\tHttps         bool   `json:\"https\"`\n\tForceHttps    bool   `json:\"force_https\"`\n}\n\ntype UpdateHttpsCertificateManagerResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tsdkResponseBaseData\n\n\t\tStatus bool `json:\"status\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) UpdateHttpsCertificateManager(req *UpdateHttpsCertificateManagerRequest) (*UpdateHttpsCertificateManagerResponse, error) {\n\treturn c.UpdateHttpsCertificateManagerWithContext(context.Background(), req)\n}\n\nfunc (c *Client) UpdateHttpsCertificateManagerWithContext(ctx context.Context, req *UpdateHttpsCertificateManagerRequest) (*UpdateHttpsCertificateManagerResponse, error) {\n\tif err := c.ensureCookieExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, \"/api/https/certificate/manager\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateHttpsCertificateManagerResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/upyun/console/api_upload_https_certificate.go",
    "content": "package console\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype UploadHttpsCertificateRequest struct {\n\tCertificate string `json:\"certificate\"`\n\tPrivateKey  string `json:\"private_key\"`\n}\n\ntype UploadHttpsCertificateResponse struct {\n\tsdkResponseBase\n\n\tData *struct {\n\t\tsdkResponseBaseData\n\n\t\tStatus int32 `json:\"status\"`\n\t\tResult struct {\n\t\t\tCertificateId string `json:\"certificate_id\"`\n\t\t\tCommonName    string `json:\"commonName\"`\n\t\t\tSerial        string `json:\"serial\"`\n\t\t} `json:\"result\"`\n\t} `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) UploadHttpsCertificate(req *UploadHttpsCertificateRequest) (*UploadHttpsCertificateResponse, error) {\n\treturn c.UploadHttpsCertificateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) UploadHttpsCertificateWithContext(ctx context.Context, req *UploadHttpsCertificateRequest) (*UploadHttpsCertificateResponse, error) {\n\tif err := c.ensureCookieExists(); err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, \"/api/https/certificate/\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UploadHttpsCertificateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/upyun/console/client.go",
    "content": "package console\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tusername string\n\tpassword string\n\n\tloginCookie    string\n\tloginCookieMtx sync.Mutex\n\n\tclient *resty.Client\n}\n\nfunc NewClient(username, password string) (*Client, error) {\n\tif username == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset username\")\n\t}\n\tif password == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset password\")\n\t}\n\n\tclient := &Client{\n\t\tusername: username,\n\t\tpassword: password,\n\t}\n\tclient.client = resty.New().\n\t\tSetBaseURL(\"https://console.upyun.com\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\tif client.loginCookie != \"\" {\n\t\t\t\treq.Header.Set(\"Cookie\", client.loginCookie)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\treturn client, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\ttresp := &sdkResponseBase{}\n\t\t\tif err := json.Unmarshal(resp.Body(), &tresp); err != nil {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t\t} else if tdata := tresp.GetData(); tdata == nil {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: received empty data\")\n\t\t\t} else if terrcode := tdata.GetErrorCode(); terrcode != 0 {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: code='%d', message='%s'\", terrcode, tdata.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) ensureCookieExists() error {\n\tc.loginCookieMtx.Lock()\n\tdefer c.loginCookieMtx.Unlock()\n\tif c.loginCookie != \"\" {\n\t\treturn nil\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPost, \"/accounts/signin/\")\n\tif err != nil {\n\t\treturn err\n\t} else {\n\t\thttpreq.SetBody(map[string]string{\n\t\t\t\"username\": c.username,\n\t\t\t\"password\": c.password,\n\t\t})\n\t}\n\n\ttype signinResponse struct {\n\t\tsdkResponseBase\n\t\tData *struct {\n\t\t\tsdkResponseBaseData\n\t\t\tResult bool `json:\"result\"`\n\t\t} `json:\"data,omitempty\"`\n\t}\n\n\tresult := &signinResponse{}\n\thttpresp, err := c.doRequestWithResult(httpreq, result)\n\tif err != nil {\n\t\treturn err\n\t} else if !result.Data.Result {\n\t\treturn errors.New(\"sdkerr: failed to signin upyun console\")\n\t} else {\n\t\tc.loginCookie = httpresp.Header().Get(\"Set-Cookie\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/upyun/console/types.go",
    "content": "package console\n\nimport (\n\t\"encoding/json\"\n)\n\ntype sdkResponse interface {\n\tGetData() *sdkResponseBaseData\n}\n\ntype sdkResponseBase struct {\n\tData *sdkResponseBaseData `json:\"data,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetData() *sdkResponseBaseData {\n\treturn r.Data\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\ntype sdkResponseBaseData struct {\n\tErrorCode json.Number `json:\"error_code,omitempty\"`\n\tMessage   string      `json:\"message,omitempty\"`\n}\n\nfunc (r *sdkResponseBaseData) GetErrorCode() int {\n\tif r.ErrorCode.String() == \"\" {\n\t\treturn 0\n\t}\n\n\terrcode, err := r.ErrorCode.Int64()\n\tif err != nil {\n\t\treturn -1\n\t}\n\n\treturn int(errcode)\n}\n\nfunc (r *sdkResponseBaseData) GetMessage() string {\n\treturn r.Message\n}\n"
  },
  {
    "path": "pkg/sdk3rd/wangsu/cdn/api_batch_update_certificate_config.go",
    "content": "package cdn\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype BatchUpdateCertificateConfigRequest struct {\n\tCertificateId int64    `json:\"certificateId\"`\n\tDomainNames   []string `json:\"domainNames\"`\n}\n\ntype BatchUpdateCertificateConfigResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) BatchUpdateCertificateConfig(req *BatchUpdateCertificateConfigRequest) (*BatchUpdateCertificateConfigResponse, error) {\n\treturn c.BatchUpdateCertificateConfigWithContext(context.Background(), req)\n}\n\nfunc (c *Client) BatchUpdateCertificateConfigWithContext(ctx context.Context, req *BatchUpdateCertificateConfigRequest) (*BatchUpdateCertificateConfigResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPut, \"/api/config/certificate/batch\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &BatchUpdateCertificateConfigResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/wangsu/cdn/client.go",
    "content": "package cdn\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/pkg/sdk3rd/wangsu/openapi\"\n)\n\ntype Client struct {\n\tclient *openapi.Client\n}\n\nfunc NewClient(accessKey, secretKey string) (*Client, error) {\n\tclient, err := openapi.NewClient(accessKey, secretKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{client: client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\treturn c.client.NewRequest(method, path)\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\treturn c.client.DoRequest(req)\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tresp, err := c.client.DoRequestWithResult(req, res)\n\tif err == nil {\n\t\tif tcode := res.GetCode(); tcode != \"\" && tcode != \"0\" {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: api error: code='%s', message='%s'\", tcode, res.GetMessage())\n\t\t}\n\t}\n\n\treturn resp, err\n}\n"
  },
  {
    "path": "pkg/sdk3rd/wangsu/cdn/types.go",
    "content": "package cdn\n\ntype sdkResponse interface {\n\tGetCode() string\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    *string `json:\"code,omitempty\"`\n\tMessage *string `json:\"message,omitempty\"`\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\nfunc (r *sdkResponseBase) GetCode() string {\n\tif r.Code == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n"
  },
  {
    "path": "pkg/sdk3rd/wangsu/cdnpro/api_create_certificate.go",
    "content": "package cdnpro\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype CreateCertificateRequest struct {\n\tTimestamp   int64                   `json:\"-\"`\n\tName        *string                 `json:\"name,omitempty\"`\n\tDescription *string                 `json:\"description,omitempty\"`\n\tAutoRenew   *string                 `json:\"autoRenew,omitempty\"`\n\tForceRenew  *bool                   `json:\"forceRenew,omitempty\"`\n\tNewVersion  *CertificateVersionInfo `json:\"newVersion,omitempty\"`\n}\n\ntype CreateCertificateResponse struct {\n\tsdkResponseBase\n\n\tCertificateLocation string `json:\"location,omitempty\"`\n}\n\nfunc (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) {\n\treturn c.CreateCertificateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) CreateCertificateWithContext(ctx context.Context, req *CreateCertificateRequest) (*CreateCertificateResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/cdn/certificates\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetHeader(\"X-CNC-Timestamp\", fmt.Sprintf(\"%d\", req.Timestamp))\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &CreateCertificateResponse{}\n\tif httpresp, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t} else {\n\t\tresult.CertificateLocation = httpresp.Header().Get(\"Location\")\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/wangsu/cdnpro/api_create_deployment_task.go",
    "content": "package cdnpro\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype CreateDeploymentTaskRequest struct {\n\tName    *string                     `json:\"name,omitempty\"`\n\tTarget  *string                     `json:\"target,omitempty\"`\n\tActions *[]DeploymentTaskActionInfo `json:\"actions,omitempty\"`\n\tWebhook *string                     `json:\"webhook,omitempty\"`\n}\n\ntype CreateDeploymentTaskResponse struct {\n\tsdkResponseBase\n\n\tDeploymentTaskLocation string `json:\"location,omitempty\"`\n}\n\nfunc (c *Client) CreateDeploymentTask(req *CreateDeploymentTaskRequest) (*CreateDeploymentTaskResponse, error) {\n\treturn c.CreateDeploymentTaskWithContext(context.Background(), req)\n}\n\nfunc (c *Client) CreateDeploymentTaskWithContext(ctx context.Context, req *CreateDeploymentTaskRequest) (*CreateDeploymentTaskResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/cdn/deploymentTasks\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &CreateDeploymentTaskResponse{}\n\tif httpresp, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t} else {\n\t\tresult.DeploymentTaskLocation = httpresp.Header().Get(\"Location\")\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/wangsu/cdnpro/api_get_deployment_task_detail.go",
    "content": "package cdnpro\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype GetDeploymentTaskDetailResponse struct {\n\tsdkResponseBase\n\n\tName           string                     `json:\"name\"`\n\tTarget         string                     `json:\"target\"`\n\tActions        []DeploymentTaskActionInfo `json:\"actions\"`\n\tStatus         string                     `json:\"status\"`\n\tStatusDetails  string                     `json:\"statusDetails\"`\n\tSubmissionTime string                     `json:\"submissionTime\"`\n\tFinishTime     string                     `json:\"finishTime\"`\n\tApiRequestId   string                     `json:\"apiRequestId\"`\n}\n\nfunc (c *Client) GetDeploymentTaskDetail(deploymentTaskId string) (*GetDeploymentTaskDetailResponse, error) {\n\treturn c.GetDeploymentTaskDetailWithContext(context.Background(), deploymentTaskId)\n}\n\nfunc (c *Client) GetDeploymentTaskDetailWithContext(ctx context.Context, deploymentTaskId string) (*GetDeploymentTaskDetailResponse, error) {\n\tif deploymentTaskId == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset deploymentTaskId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf(\"/cdn/deploymentTasks/%s\", url.PathEscape(deploymentTaskId)))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &GetDeploymentTaskDetailResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/wangsu/cdnpro/api_get_hostname_detail.go",
    "content": "package cdnpro\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype GetHostnameDetailResponse struct {\n\tsdkResponseBase\n\n\tHostname             string                `json:\"hostname\"`\n\tPropertyInProduction *HostnamePropertyInfo `json:\"propertyInProduction,omitempty\"`\n\tPropertyInStaging    *HostnamePropertyInfo `json:\"propertyInStaging,omitempty\"`\n}\n\nfunc (c *Client) GetHostnameDetail(hostname string) (*GetHostnameDetailResponse, error) {\n\treturn c.GetHostnameDetailWithContext(context.Background(), hostname)\n}\n\nfunc (c *Client) GetHostnameDetailWithContext(ctx context.Context, hostname string) (*GetHostnameDetailResponse, error) {\n\tif hostname == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset hostname\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf(\"/cdn/hostnames/%s\", url.PathEscape(hostname)))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &GetHostnameDetailResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/wangsu/cdnpro/api_update_certificate.go",
    "content": "package cdnpro\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype UpdateCertificateRequest struct {\n\tTimestamp   int64                   `json:\"-\"`\n\tName        *string                 `json:\"name,omitempty\"`\n\tDescription *string                 `json:\"description,omitempty\"`\n\tAutoRenew   *string                 `json:\"autoRenew,omitempty\"`\n\tForceRenew  *bool                   `json:\"forceRenew,omitempty\"`\n\tNewVersion  *CertificateVersionInfo `json:\"newVersion,omitempty\"`\n}\n\ntype UpdateCertificateResponse struct {\n\tsdkResponseBase\n\n\tCertificateLocation string `json:\"location,omitempty\"`\n}\n\nfunc (c *Client) UpdateCertificate(certificateId string, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {\n\treturn c.UpdateCertificateWithContext(context.Background(), certificateId, req)\n}\n\nfunc (c *Client) UpdateCertificateWithContext(ctx context.Context, certificateId string, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {\n\tif certificateId == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset certificateId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPatch, fmt.Sprintf(\"/cdn/certificates/%s\", url.PathEscape(certificateId)))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetHeader(\"X-CNC-Timestamp\", fmt.Sprintf(\"%d\", req.Timestamp))\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateCertificateResponse{}\n\tif httpresp, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t} else {\n\t\tresult.CertificateLocation = httpresp.Header().Get(\"Location\")\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/wangsu/cdnpro/client.go",
    "content": "package cdnpro\n\nimport (\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/pkg/sdk3rd/wangsu/openapi\"\n)\n\ntype Client struct {\n\tclient *openapi.Client\n}\n\nfunc NewClient(accessKey, secretKey string) (*Client, error) {\n\tclient, err := openapi.NewClient(accessKey, secretKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{client: client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\treturn c.client.NewRequest(method, path)\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\treturn c.client.DoRequest(req)\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\treturn c.client.DoRequestWithResult(req, res)\n}\n"
  },
  {
    "path": "pkg/sdk3rd/wangsu/cdnpro/types.go",
    "content": "package cdnpro\n\ntype sdkResponse interface {\n\tGetCode() string\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    *string `json:\"code,omitempty\"`\n\tMessage *string `json:\"message,omitempty\"`\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\nfunc (r *sdkResponseBase) GetCode() string {\n\tif r.Code == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\ntype CertificateVersionInfo struct {\n\tComments           *string                               `json:\"comments,omitempty\"`\n\tPrivateKey         *string                               `json:\"privateKey,omitempty\"`\n\tCertificate        *string                               `json:\"certificate,omitempty\"`\n\tChainCert          *string                               `json:\"chainCert,omitempty\"`\n\tIdentificationInfo *CertificateVersionIdentificationInfo `json:\"identificationInfo,omitempty\"`\n}\n\ntype CertificateVersionIdentificationInfo struct {\n\tCountry                 *string   `json:\"country,omitempty\"`\n\tState                   *string   `json:\"state,omitempty\"`\n\tCity                    *string   `json:\"city,omitempty\"`\n\tCompany                 *string   `json:\"company,omitempty\"`\n\tDepartment              *string   `json:\"department,omitempty\"`\n\tCommonName              *string   `json:\"commonName,omitempty\"`\n\tEmail                   *string   `json:\"email,omitempty\"`\n\tSubjectAlternativeNames *[]string `json:\"subjectAlternativeNames,omitempty\"`\n}\n\ntype HostnamePropertyInfo struct {\n\tPropertyId    string  `json:\"propertyId\"`\n\tVersion       int32   `json:\"version\"`\n\tCertificateId *string `json:\"certificateId,omitempty\"`\n}\n\ntype DeploymentTaskActionInfo struct {\n\tAction        *string `json:\"action,omitempty\"`\n\tPropertyId    *string `json:\"propertyId,omitempty\"`\n\tCertificateId *string `json:\"certificateId,omitempty\"`\n\tVersion       *int32  `json:\"version,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/wangsu/certificate/api_create_certificate.go",
    "content": "package certificate\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype CreateCertificateRequest struct {\n\tName        *string `json:\"name,omitempty\"`\n\tCertificate *string `json:\"certificate,omitempty\"`\n\tPrivateKey  *string `json:\"privateKey,omitempty\"`\n\tComment     *string `json:\"comment,omitempty\" `\n}\n\ntype CreateCertificateResponse struct {\n\tsdkResponseBase\n\n\tCertificateLocation string `json:\"location,omitempty\"`\n}\n\nfunc (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) {\n\treturn c.CreateCertificateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) CreateCertificateWithContext(ctx context.Context, req *CreateCertificateRequest) (*CreateCertificateResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodPost, \"/api/certificate\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &CreateCertificateResponse{}\n\tif httpresp, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t} else {\n\t\tresult.CertificateLocation = httpresp.Header().Get(\"Location\")\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/wangsu/certificate/api_list_certificates.go",
    "content": "package certificate\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype ListCertificatesResponse struct {\n\tsdkResponseBase\n\n\tCertificates []*CertificateRecord `json:\"ssl-certificates,omitempty\"`\n}\n\nfunc (c *Client) ListCertificates() (*ListCertificatesResponse, error) {\n\treturn c.ListCertificatesWithContext(context.Background())\n}\n\nfunc (c *Client) ListCertificatesWithContext(ctx context.Context) (*ListCertificatesResponse, error) {\n\thttpreq, err := c.newRequest(http.MethodGet, \"/api/ssl/certificate\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &ListCertificatesResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/wangsu/certificate/api_update_certificate.go",
    "content": "package certificate\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype UpdateCertificateRequest struct {\n\tName        *string `json:\"name,omitempty\"`\n\tCertificate *string `json:\"certificate,omitempty\"`\n\tPrivateKey  *string `json:\"privateKey,omitempty\"`\n\tComment     *string `json:\"comment,omitempty\" `\n}\n\ntype UpdateCertificateResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) UpdateCertificate(certificateId string, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {\n\treturn c.UpdateCertificateWithContext(context.Background(), certificateId, req)\n}\n\nfunc (c *Client) UpdateCertificateWithContext(ctx context.Context, certificateId string, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {\n\tif certificateId == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset certificateId\")\n\t}\n\n\thttpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf(\"/api/certificate/%s\", url.PathEscape(certificateId)))\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &UpdateCertificateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/wangsu/certificate/client.go",
    "content": "package certificate\n\nimport (\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/pkg/sdk3rd/wangsu/openapi\"\n)\n\ntype Client struct {\n\tclient *openapi.Client\n}\n\nfunc NewClient(accessKey, secretKey string) (*Client, error) {\n\tclient, err := openapi.NewClient(accessKey, secretKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{client: client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(method string, path string) (*resty.Request, error) {\n\treturn c.client.NewRequest(method, path)\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\treturn c.client.DoRequest(req)\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\treturn c.client.DoRequestWithResult(req, res)\n}\n"
  },
  {
    "path": "pkg/sdk3rd/wangsu/certificate/types.go",
    "content": "package certificate\n\ntype sdkResponse interface {\n\tGetCode() string\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode    *string `json:\"code,omitempty\"`\n\tMessage *string `json:\"message,omitempty\"`\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n\nfunc (r *sdkResponseBase) GetCode() string {\n\tif r.Code == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\ntype CertificateRecord struct {\n\tCertificateId string `json:\"certificate-id\"`\n\tName          string `json:\"name\"`\n\tComment       string `json:\"comment\"`\n\tValidityFrom  string `json:\"certificate-validity-from\"`\n\tValidityTo    string `json:\"certificate-validity-to\"`\n\tSerial        string `json:\"certificate-serial\"`\n}\n"
  },
  {
    "path": "pkg/sdk3rd/wangsu/openapi/client.go",
    "content": "package openapi\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\taccessKey string\n\tsecretKey string\n\n\tclient *resty.Client\n}\n\nfunc NewClient(accessKey, secretKey string) (*Client, error) {\n\tif accessKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset accessKey\")\n\t}\n\tif secretKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset secretKey\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(\"https://open.chinanetcenter.com\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"Host\", \"open.chinanetcenter.com\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\t// Step 1: Get request method\n\t\t\tmethod := req.Method\n\t\t\tmethod = strings.ToUpper(method)\n\n\t\t\t// Step 2: Get request path\n\t\t\tpath := \"/\"\n\t\t\tif req.URL != nil {\n\t\t\t\tpath = req.URL.Path\n\t\t\t}\n\n\t\t\t// Step 3: Get unencoded query string\n\t\t\tqueryString := \"\"\n\t\t\tif method != http.MethodPost && req.URL != nil {\n\t\t\t\tqueryString = req.URL.RawQuery\n\n\t\t\t\ts, err := url.QueryUnescape(queryString)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tqueryString = s\n\t\t\t}\n\n\t\t\t// Step 4: Get canonical headers & signed headers\n\t\t\tcanonicalHeaders := \"\" +\n\t\t\t\t\"content-type:\" + strings.TrimSpace(strings.ToLower(req.Header.Get(\"Content-Type\"))) + \"\\n\" +\n\t\t\t\t\"host:\" + strings.TrimSpace(strings.ToLower(req.Header.Get(\"Host\"))) + \"\\n\"\n\t\t\tsignedHeaders := \"content-type;host\"\n\n\t\t\t// Step 5: Get request payload\n\t\t\tpayload := \"\"\n\t\t\tif method != http.MethodGet && req.Body != nil {\n\t\t\t\treader, err := req.GetBody()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tdefer reader.Close()\n\n\t\t\t\tpayloadb, err := io.ReadAll(reader)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tpayload = string(payloadb)\n\t\t\t}\n\t\t\thashedPayload := sha256.Sum256([]byte(payload))\n\t\t\thashedPayloadHex := strings.ToLower(hex.EncodeToString(hashedPayload[:]))\n\n\t\t\t// Step 6: Get timestamp\n\t\t\tvar reqtime time.Time\n\t\t\ttimestampString := req.Header.Get(\"X-CNC-Timestamp\")\n\t\t\tif timestampString == \"\" {\n\t\t\t\treqtime = time.Now().UTC()\n\t\t\t\ttimestampString = fmt.Sprintf(\"%d\", reqtime.Unix())\n\t\t\t} else {\n\t\t\t\ttimestamp, err := strconv.ParseInt(timestampString, 10, 64)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treqtime = time.Unix(timestamp, 0).UTC()\n\t\t\t}\n\n\t\t\t// Step 7: Get canonical request string\n\t\t\tcanonicalRequest := fmt.Sprintf(\"%s\\n%s\\n%s\\n%s\\n%s\\n%s\", method, path, queryString, canonicalHeaders, signedHeaders, hashedPayloadHex)\n\t\t\thashedCanonicalRequest := sha256.Sum256([]byte(canonicalRequest))\n\t\t\thashedCanonicalRequestHex := strings.ToLower(hex.EncodeToString(hashedCanonicalRequest[:]))\n\n\t\t\t// Step 8: String to sign\n\t\t\tconst SignAlgorithmHeader = \"CNC-HMAC-SHA256\"\n\t\t\tstringToSign := fmt.Sprintf(\"%s\\n%s\\n%s\", SignAlgorithmHeader, timestampString, hashedCanonicalRequestHex)\n\t\t\thmac := hmac.New(sha256.New, []byte(secretKey))\n\t\t\thmac.Write([]byte(stringToSign))\n\t\t\tsign := hmac.Sum(nil)\n\t\t\tsignHex := strings.ToLower(hex.EncodeToString(sign))\n\n\t\t\t// Step 9: Add headers to request\n\t\t\treq.Header.Set(\"X-CNC-AccessKey\", accessKey)\n\t\t\treq.Header.Set(\"X-CNC-Timestamp\", timestampString)\n\t\t\treq.Header.Set(\"X-CNC-Auth-Method\", \"AKSK\")\n\t\t\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"%s Credential=%s, SignedHeaders=%s, Signature=%s\", SignAlgorithmHeader, accessKey, signedHeaders, signHex))\n\t\t\treq.Header.Set(\"Date\", reqtime.Format(\"Mon, 02 Jan 2006 15:04:05 GMT\"))\n\n\t\t\treturn nil\n\t\t})\n\n\treturn &Client{\n\t\taccessKey: accessKey,\n\t\tsecretKey: secretKey,\n\t\tclient:    client,\n\t}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) NewRequest(method string, path string) (*resty.Request, error) {\n\tif method == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset method\")\n\t}\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = method\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) DoRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) DoRequestWithResult(req *resty.Request, res interface{}) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.DoRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/xinnet/api_dns_create.go",
    "content": "package xinnet\n\nimport (\n\t\"context\"\n)\n\ntype DnsCreateRequest struct {\n\tDomainName *string `json:\"domainName,omitempty\"`\n\tRecordName *string `json:\"recordName,omitempty\"`\n\tType       *string `json:\"type,omitempty\"`\n\tValue      *string `json:\"value,omitempty\"`\n\tLine       *string `json:\"line,omitempty\"`\n\tTtl        *int32  `json:\"ttl,omitempty\"`\n\tMx         *int32  `json:\"mx,omitempty\"`\n\tStatus     *int32  `json:\"status,omitempty\"`\n}\n\ntype DnsCreateResponse struct {\n\tsdkResponseBase\n\tData *int64 `json:\"data,omitempty\"`\n}\n\nfunc (c *Client) DnsCreate(req *DnsCreateRequest) (*DnsCreateResponse, error) {\n\treturn c.DnsCreateWithContext(context.Background(), req)\n}\n\nfunc (c *Client) DnsCreateWithContext(ctx context.Context, req *DnsCreateRequest) (*DnsCreateResponse, error) {\n\thttpreq, err := c.newRequest(\"/dns/create/\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &DnsCreateResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/xinnet/api_dns_delete.go",
    "content": "package xinnet\n\nimport (\n\t\"context\"\n)\n\ntype DnsDeleteRequest struct {\n\tDomainName *string `json:\"domainName,omitempty\"`\n\tRecordId   *int64  `json:\"recordId,omitempty\"`\n}\n\ntype DnsDeleteResponse struct {\n\tsdkResponseBase\n}\n\nfunc (c *Client) DnsDelete(req *DnsDeleteRequest) (*DnsDeleteResponse, error) {\n\treturn c.DnsDeleteWithContext(context.Background(), req)\n}\n\nfunc (c *Client) DnsDeleteWithContext(ctx context.Context, req *DnsDeleteRequest) (*DnsDeleteResponse, error) {\n\thttpreq, err := c.newRequest(\"/dns/delete/\")\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\thttpreq.SetBody(req)\n\t\thttpreq.SetContext(ctx)\n\t}\n\n\tresult := &DnsDeleteResponse{}\n\tif _, err := c.doRequestWithResult(httpreq, result); err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/xinnet/client.go",
    "content": "package xinnet\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-resty/resty/v2\"\n\n\t\"github.com/certimate-go/certimate/internal/app\"\n)\n\ntype Client struct {\n\tclient *resty.Client\n}\n\nfunc NewClient(agentId, appSecret string) (*Client, error) {\n\tif agentId == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset agentId\")\n\t}\n\tif appSecret == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset appSecret\")\n\t}\n\n\tclient := resty.New().\n\t\tSetBaseURL(\"https://apiv2.xinnet.com/api\").\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"User-Agent\", app.AppUserAgent).\n\t\tSetPreRequestHook(func(c *resty.Client, req *http.Request) error {\n\t\t\t// 生成时间戳\n\t\t\ttimestamp := time.Now().UTC().Format(\"20060102T150405Z\")\n\n\t\t\t// 获取请求路径，注意结尾必须是 \"/\"\n\t\t\turlPath := \"/\"\n\t\t\tif req.URL != nil {\n\t\t\t\turlPath = req.URL.Path\n\n\t\t\t\tif !strings.HasSuffix(urlPath, \"/\") {\n\t\t\t\t\turlPath += \"/\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 获取请求方法\n\t\t\trequestMethod := req.Method\n\n\t\t\t// 获取请求体\n\t\t\trequestBody := \"\"\n\t\t\tif req.Body != nil {\n\t\t\t\treader, err := req.GetBody()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tdefer reader.Close()\n\n\t\t\t\tpayloadb, err := io.ReadAll(reader)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\trequestBody = string(payloadb)\n\t\t\t}\n\n\t\t\t// 计算签名\n\t\t\talgorithm := \"HMAC-SHA256\"\n\t\t\tstringToSign := algorithm + \"\\n\" +\n\t\t\t\ttimestamp + \"\\n\" +\n\t\t\t\trequestMethod + \"\\n\" +\n\t\t\t\turlPath + \"\\n\" +\n\t\t\t\trequestBody\n\t\t\th := hmac.New(sha256.New, []byte(appSecret))\n\t\t\th.Write([]byte(stringToSign))\n\t\t\tsignature := hex.EncodeToString(h.Sum(nil))\n\n\t\t\t// 设置请求头\n\t\t\treq.Header.Set(\"timestamp\", timestamp)\n\t\t\treq.Header.Set(\"authorization\", fmt.Sprintf(\"%s Access=%s, Signature=%s\", algorithm, agentId, signature))\n\n\t\t\treturn nil\n\t\t})\n\n\treturn &Client{client}, nil\n}\n\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.client.SetTimeout(timeout)\n\treturn c\n}\n\nfunc (c *Client) newRequest(path string) (*resty.Request, error) {\n\tif path == \"\" {\n\t\treturn nil, fmt.Errorf(\"sdkerr: unset path\")\n\t}\n\n\treq := c.client.R()\n\treq.Method = http.MethodPost\n\treq.URL = path\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\t// WARN:\n\t//   PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.\n\n\tresp, err := req.Send()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"sdkerr: failed to send request: %w\", err)\n\t} else if resp.IsError() {\n\t\treturn resp, fmt.Errorf(\"sdkerr: unexpected status code: %d (resp: %s)\", resp.StatusCode(), resp.String())\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {\n\tif req == nil {\n\t\treturn nil, fmt.Errorf(\"sdkerr: nil request\")\n\t}\n\n\tresp, err := c.doRequest(req)\n\tif err != nil {\n\t\tif resp != nil {\n\t\t\tjson.Unmarshal(resp.Body(), &res)\n\t\t}\n\t\treturn resp, err\n\t}\n\n\tif len(resp.Body()) != 0 {\n\t\tif err := json.Unmarshal(resp.Body(), &res); err != nil {\n\t\t\treturn resp, fmt.Errorf(\"sdkerr: failed to unmarshal response: %w (resp: %s)\", err, resp.String())\n\t\t} else {\n\t\t\tif tcode := res.GetCode(); tcode != \"0\" {\n\t\t\t\treturn resp, fmt.Errorf(\"sdkerr: code='%s', msg='%s'\", tcode, res.GetMessage())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/sdk3rd/xinnet/types.go",
    "content": "package xinnet\n\ntype sdkResponse interface {\n\tGetCode() string\n\tGetMessage() string\n}\n\ntype sdkResponseBase struct {\n\tCode      *string `json:\"code,omitempty\"`\n\tMessage   *string `json:\"message,omitempty\"`\n\tRequestId *string `json:\"requestId,omitempty\"`\n}\n\nfunc (r *sdkResponseBase) GetCode() string {\n\tif r.Code == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Code\n}\n\nfunc (r *sdkResponseBase) GetMessage() string {\n\tif r.Message == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *r.Message\n}\n\nvar _ sdkResponse = (*sdkResponseBase)(nil)\n"
  },
  {
    "path": "pkg/utils/cert/common.go",
    "content": "package cert\n\nimport (\n\t\"encoding/pem\"\n)\n\nfunc decodePEMBlocks(data []byte) []*pem.Block {\n\tblocks := make([]*pem.Block, 0)\n\tfor {\n\t\tblock, rest := pem.Decode(data)\n\t\tif block == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tblocks = append(blocks, block)\n\t\tdata = rest\n\t}\n\n\treturn blocks\n}\n"
  },
  {
    "path": "pkg/utils/cert/comparer.go",
    "content": "package cert\n\nimport (\n\t\"crypto/x509\"\n)\n\n// 比较两个 x509.Certificate 对象，判断它们是否是同一张证书。\n//\n// 入参:\n//   - a: 待比较的第一个 x509.Certificate 对象。\n//   - b: 待比较的第二个 x509.Certificate 对象。\n//\n// 出参:\n//   - 是否相同。\nfunc EqualCertificates(a, b *x509.Certificate) bool {\n\tif a == nil || b == nil {\n\t\treturn false\n\t}\n\n\treturn a.Equal(b)\n}\n\n// 与 [EqualCertificates] 方法类似，但入参是 PEM 编码的证书字符串。\n//\n// 入参:\n//   - a: 待比较的第一个证书 PEM 内容。\n//   - b: 待比较的第二个证书 PEM 内容。\n//\n// 出参:\n//   - 是否相同。\nfunc EqualCertificatesFromPEM(a, b string) bool {\n\taCert, _ := ParseCertificateFromPEM(a)\n\tbCert, _ := ParseCertificateFromPEM(b)\n\treturn EqualCertificates(aCert, bCert)\n}\n"
  },
  {
    "path": "pkg/utils/cert/converter.go",
    "content": "package cert\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"fmt\"\n)\n\n// 将 x509.Certificate 对象转换为 PEM 编码的字符串。\n//\n// 入参:\n//   - cert: x509.Certificate 对象。\n//\n// 出参:\n//   - certPEM: 证书 PEM 内容。\n//   - err: 错误。\nfunc ConvertCertificateToPEM(cert *x509.Certificate) (_certPEM string, _err error) {\n\tif cert == nil {\n\t\treturn \"\", errors.New(\"the input certificate is nil\")\n\t}\n\n\tblock := &pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: cert.Raw,\n\t}\n\n\treturn string(pem.EncodeToMemory(block)), nil\n}\n\n// 将 ecdsa.PrivateKey 对象转换为 PEM 编码的字符串。\n//\n// 入参:\n//   - privkey: ecdsa.PrivateKey 对象。\n//\n// 出参:\n//   - privkeyPEM: 私钥 PEM 内容。\n//   - err: 错误。\nfunc ConvertECPrivateKeyToPEM(privkey *ecdsa.PrivateKey) (_privkeyPEM string, _err error) {\n\tif privkey == nil {\n\t\treturn \"\", errors.New(\"the input private key is nil\")\n\t}\n\n\tdata, _err := x509.MarshalECPrivateKey(privkey)\n\tif _err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal EC private key: %w\", _err)\n\t}\n\n\tblock := &pem.Block{\n\t\tType:  \"EC PRIVATE KEY\",\n\t\tBytes: data,\n\t}\n\n\treturn string(pem.EncodeToMemory(block)), nil\n}\n"
  },
  {
    "path": "pkg/utils/cert/extractor.go",
    "content": "package cert\n\nimport (\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"fmt\"\n)\n\n// 从 PEM 编码的证书字符串解析并提取服务器证书和中间证书。\n//\n// 入参:\n//   - certPEM: 证书 PEM 内容。\n//\n// 出参:\n//   - serverCertPEM: 服务器证书的 PEM 内容。\n//   - intermediaCertPEM: 中间证书的 PEM 内容。\n//   - err: 错误。\nfunc ExtractCertificatesFromPEM(certPEM string) (_serverCertPEM string, _intermediaCertPEM string, _err error) {\n\tblocks := decodePEMBlocks([]byte(certPEM))\n\tfor i, block := range blocks {\n\t\tif block.Type != \"CERTIFICATE\" {\n\t\t\treturn \"\", \"\", fmt.Errorf(\"invalid PEM block type at %d, expected 'CERTIFICATE', got '%s'\", i, block.Type)\n\t\t}\n\t}\n\n\tserverCertPEM := \"\"\n\tintermediaCertPEM := \"\"\n\n\tif len(blocks) == 0 {\n\t\treturn \"\", \"\", errors.New(\"failed to decode PEM block\")\n\t}\n\n\tif len(blocks) > 0 {\n\t\tserverCertPEM = string(pem.EncodeToMemory(blocks[0]))\n\t}\n\n\tif len(blocks) > 1 {\n\t\tfor i := 1; i < len(blocks); i++ {\n\t\t\tintermediaCertPEM += string(pem.EncodeToMemory(blocks[i]))\n\t\t}\n\t}\n\n\treturn serverCertPEM, intermediaCertPEM, nil\n}\n"
  },
  {
    "path": "pkg/utils/cert/hostname/hostname.go",
    "content": "package hostname\n\nimport (\n\t\"crypto/x509\"\n\t\"net\"\n\t\"strings\"\n)\n\n// 检查目标主机名是否匹配待匹配主机名。\n//\n// 入参：\n//   - match: 待匹配主机名。可以是泛域名，如 \"*.example.com\"。\n//   - candidate: 目标主机名。如 \"sub.example.com\"。\n//\n// 出参：\n//   - 是否匹配。\nfunc IsMatch(match, candidate string) bool {\n\tif match == \"\" || candidate == \"\" {\n\t\treturn false\n\t}\n\n\tmockCert := &x509.Certificate{}\n\tif ip := net.ParseIP(match); ip != nil {\n\t\tmockCert.IPAddresses = []net.IP{ip}\n\t} else {\n\t\tif strings.EqualFold(match, candidate) {\n\t\t\treturn true\n\t\t}\n\t\tmockCert.DNSNames = []string{match}\n\t}\n\treturn mockCert.VerifyHostname(candidate) == nil\n}\n"
  },
  {
    "path": "pkg/utils/cert/hostname/hostname_test.go",
    "content": "package hostname_test\n\nimport (\n\t\"testing\"\n\n\txcerthostname \"github.com/certimate-go/certimate/pkg/utils/cert/hostname\"\n)\n\nfunc TestCertHostnameUtil_IsMatch(t *testing.T) {\n\tt.Run(\"IsMatch\", func(t *testing.T) {\n\t\ttestCases := []struct {\n\t\t\twildcard string\n\t\t\ttarget   string\n\t\t\texpected bool\n\t\t}{\n\t\t\t{\"*.example.com\", \"sub.example.com\", true},\n\t\t\t{\"*.example.com\", \"sub.sub.example.com\", false},\n\t\t\t{\"*.example.com\", \"example.com\", false},\n\n\t\t\t{\"*.*.example.com\", \"a.b.example.com\", false},\n\t\t\t{\"*.*.example.com\", \"a.example.com\", false},\n\t\t\t{\"*.*.example.com\", \"a.b.c.example.com\", false},\n\n\t\t\t{\"example.com\", \"example.com\", true},\n\t\t\t{\"example.com\", \"wrong.com\", false},\n\n\t\t\t{\"\", \"example.com\", false},\n\t\t\t{\"*.example.com\", \"\", false},\n\n\t\t\t{\"*.sub.example.com\", \"a.sub.example.com\", true},\n\t\t\t{\"*.sub.example.com\", \"a.b.sub.example.com\", false},\n\t\t\t{\"*.sub.example.com\", \"sub.example.com\", false},\n\n\t\t\t{\"*.Example.COM\", \"sub.example.com\", true},\n\t\t\t{\"*.EXAMPLE.COM\", \"SUB.EXAMPLE.COM\", true},\n\t\t}\n\n\t\tfor _, tc := range testCases {\n\t\t\tresult := xcerthostname.IsMatch(tc.wildcard, tc.target)\n\t\t\tstatus := \"✓\"\n\t\t\tpf := t.Logf\n\t\t\tif result != tc.expected {\n\t\t\t\tstatus = \"✗\"\n\t\t\t\tpf = t.Errorf\n\t\t\t}\n\n\t\t\tpf(\"%s Wildcard: %-20s Target: %-20s Expected: %-5v Got: %-5v\\n\", status, tc.wildcard, tc.target, tc.expected, result)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "pkg/utils/cert/key/key.go",
    "content": "package key\n\nimport (\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"errors\"\n)\n\ntype KeyAlgorithm = x509.PublicKeyAlgorithm\n\nfunc GetPublicKeyAlgorithm(pubkey crypto.PublicKey) (_algorithm KeyAlgorithm, _size int, _error error) {\n\tswitch t := pubkey.(type) {\n\tcase *rsa.PublicKey:\n\t\tsize := t.N.BitLen()\n\t\treturn x509.RSA, size, nil\n\n\tcase *ecdsa.PublicKey:\n\t\tsize := t.Curve.Params().BitSize\n\t\treturn x509.ECDSA, size, nil\n\n\tcase ed25519.PublicKey:\n\t\treturn x509.Ed25519, 256, nil\n\t}\n\n\treturn x509.UnknownPublicKeyAlgorithm, 0, errors.New(\"unknown public key type\")\n}\n\nfunc GetPrivateKeyAlgorithm(privkey crypto.PrivateKey) (_algorithm KeyAlgorithm, _size int, _error error) {\n\tswitch t := privkey.(type) {\n\tcase *rsa.PrivateKey:\n\t\tsize := t.N.BitLen()\n\t\treturn x509.RSA, size, nil\n\n\tcase *ecdsa.PrivateKey:\n\t\tsize := t.Curve.Params().BitSize\n\t\treturn x509.ECDSA, size, nil\n\n\tcase ed25519.PrivateKey:\n\t\treturn x509.Ed25519, 512, nil\n\t}\n\n\treturn x509.UnknownPublicKeyAlgorithm, 0, errors.New(\"unknown private key type\")\n}\n"
  },
  {
    "path": "pkg/utils/cert/parser.go",
    "content": "package cert\n\nimport (\n\t\"crypto\"\n\t\"crypto/x509\"\n\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n)\n\n// 从 PEM 编码的证书字符串解析并返回一个 x509.Certificate 对象。\n// PEM 内容可能是包含多张证书的证书链，但只返回第一个证书（即服务器证书）。\n//\n// 入参:\n//   - certPEM: 证书 PEM 内容。\n//\n// 出参:\n//   - cert: x509.Certificate 对象。\n//   - err: 错误。\nfunc ParseCertificateFromPEM(certPEM string) (_cert *x509.Certificate, _err error) {\n\treturn certcrypto.ParsePEMCertificate([]byte(certPEM))\n}\n\n// 从 PEM 编码的私钥字符串解析并返回一个 crypto.PrivateKey 对象。\n//\n// 入参:\n//   - privkeyPEM: 私钥 PEM 内容。\n//\n// 出参:\n//   - privkey: crypto.PrivateKey 对象，可能是 rsa.PrivateKey、ecdsa.PrivateKey 或 ed25519.PrivateKey。\n//   - err: 错误。\nfunc ParsePrivateKeyFromPEM(privkeyPEM string) (_privkey crypto.PrivateKey, _err error) {\n\treturn certcrypto.ParsePEMPrivateKey([]byte(privkeyPEM))\n}\n"
  },
  {
    "path": "pkg/utils/cert/transformer.go",
    "content": "package cert\n\nimport (\n\t\"bytes\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/pavlo-v-chernykh/keystore-go/v4\"\n\t\"software.sslmate.com/src/go-pkcs12\"\n)\n\n// 将 PEM 编码的证书字符串转换为 PFX 格式。\n//\n// 入参:\n//   - certPEM: 证书 PEM 内容。\n//   - privkeyPEM: 私钥 PEM 内容。\n//   - pfxPassword: PFX 导出密码。\n//\n// 出参:\n//   - data: PFX 格式的证书数据。\n//   - err: 错误。\nfunc TransformCertificateFromPEMToPFX(certPEM string, privkeyPEM string, pfxPassword string) ([]byte, error) {\n\tblocks := decodePEMBlocks([]byte(certPEM))\n\n\tcerts := make([]*x509.Certificate, 0, len(blocks))\n\tfor i, block := range blocks {\n\t\tif block.Type != \"CERTIFICATE\" {\n\t\t\treturn nil, fmt.Errorf(\"invalid PEM block type at %d, expected 'CERTIFICATE', got '%s'\", i, block.Type)\n\t\t}\n\n\t\tcert, err := x509.ParseCertificate(block.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcerts = append(certs, cert)\n\t}\n\n\tprivkey, err := ParsePrivateKeyFromPEM(privkeyPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar pfxData []byte\n\tif len(certs) == 0 {\n\t\treturn nil, errors.New(\"failed to decode certificate PEM\")\n\t} else if len(certs) == 1 {\n\t\tpfxData, err = pkcs12.Legacy.Encode(privkey, certs[0], nil, pfxPassword)\n\t} else {\n\t\tpfxData, err = pkcs12.Legacy.Encode(privkey, certs[0], certs[1:], pfxPassword)\n\t}\n\n\treturn pfxData, err\n}\n\n// 将 PEM 编码的证书字符串转换为 JKS 格式。\n//\n// 入参:\n//   - certPEM: 证书 PEM 内容。\n//   - privkeyPEM: 私钥 PEM 内容。\n//   - jksAlias: JKS 别名。\n//   - jksKeypass: JKS 密钥密码。\n//   - jksStorepass: JKS 存储密码。\n//\n// 出参:\n//   - data: JKS 格式的证书数据。\n//   - err: 错误。\nfunc TransformCertificateFromPEMToJKS(certPEM string, privkeyPEM string, jksAlias string, jksKeypass string, jksStorepass string) ([]byte, error) {\n\tcertBlocks := decodePEMBlocks([]byte(certPEM))\n\tif len(certBlocks) == 0 {\n\t\treturn nil, errors.New(\"failed to decode certificate PEM\")\n\t}\n\n\tprivkeyBlocks := decodePEMBlocks([]byte(privkeyPEM))\n\tif len(privkeyBlocks) == 0 {\n\t\treturn nil, errors.New(\"failed to decode private key PEM\")\n\t}\n\n\tentry := keystore.PrivateKeyEntry{\n\t\tCreationTime:     time.Now(),\n\t\tPrivateKey:       privkeyBlocks[0].Bytes,\n\t\tCertificateChain: make([]keystore.Certificate, len(certBlocks)),\n\t}\n\tfor i, certBlock := range certBlocks {\n\t\tif certBlock.Type != \"CERTIFICATE\" {\n\t\t\treturn nil, fmt.Errorf(\"invalid PEM block type at %d, expected 'CERTIFICATE', got '%s'\", i, certBlock.Type)\n\t\t}\n\n\t\tentry.CertificateChain[i] = keystore.Certificate{\n\t\t\tType:    \"X509\",\n\t\t\tContent: certBlock.Bytes,\n\t\t}\n\t}\n\n\tks := keystore.New()\n\tif err := ks.SetPrivateKeyEntry(jksAlias, entry, []byte(jksKeypass)); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar buf bytes.Buffer\n\tif err := ks.Store(&buf, []byte(jksStorepass)); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn buf.Bytes(), nil\n}\n"
  },
  {
    "path": "pkg/utils/cert/x509/x509.go",
    "content": "﻿package x509\n\nimport (\n\t\"crypto/x509\"\n\t\"encoding/asn1\"\n\t\"net\"\n)\n\nvar oidSubjectAlternativeNameExtension = asn1.ObjectIdentifier{2, 5, 29, 17}\n\nconst (\n\tsanGeneralNameTagEmail = 1\n\tsanGeneralNameTagDNS   = 2\n\tsanGeneralNameTagURI   = 6\n\tsanGeneralNameTagIP    = 7\n)\n\n// 返回指定 x509.Certificate 对象的主题名称。\n// 如果主题名称为空，则返回第一个主题替代名称。\n//\n// 入参：\n//   - cert: x509.Certificate 对象。\n//\n// 出参：\n//   - 主题名称。\nfunc GetSubjectCommonName(cert *x509.Certificate) string {\n\tif cert != nil {\n\t\tif cert.Subject.CommonName != \"\" {\n\t\t\treturn cert.Subject.CommonName\n\t\t}\n\n\t\tsans := GetSubjectAltNames(cert)\n\t\tif len(sans) > 0 {\n\t\t\treturn sans[0]\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n// 返回指定 x509.Certificate 对象的主题替代名称。\n//\n// 入参：\n//   - cert: x509.Certificate 对象。\n//\n// 出参：\n//   - 主题替代名称的字符串切片。\nfunc GetSubjectAltNames(cert *x509.Certificate) []string {\n\tsans := make([]string, 0)\n\n\tif cert != nil {\n\t\t// 注意，这里不直接使用 `DNSNames`、`IPAddresses` 等字段，以保证原始顺序不变\n\t\tfor _, ext := range cert.Extensions {\n\t\t\tif ext.Id.Equal(oidSubjectAlternativeNameExtension) {\n\t\t\t\tvar seq asn1.RawValue\n\t\t\t\tif _, err := asn1.Unmarshal(ext.Value, &seq); err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\trest := seq.Bytes\n\t\t\t\tfor len(rest) > 0 {\n\t\t\t\t\tvar name asn1.RawValue\n\t\t\t\t\tvar err error\n\t\t\t\t\trest, err = asn1.Unmarshal(rest, &name)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\n\t\t\t\t\tswitch name.Tag {\n\t\t\t\t\tcase sanGeneralNameTagIP:\n\t\t\t\t\t\tvar ip net.IP = name.Bytes\n\t\t\t\t\t\tsans = append(sans, ip.String())\n\n\t\t\t\t\tcase sanGeneralNameTagEmail, sanGeneralNameTagDNS, sanGeneralNameTagURI:\n\t\t\t\t\t\tsans = append(sans, string(name.Bytes))\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\t// 忽略其他非 Critical 的 GeneralName​\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn sans\n}\n"
  },
  {
    "path": "pkg/utils/crypto/aes.go",
    "content": "package crypto\n\nimport (\n\t\"bytes\"\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"io\"\n)\n\ntype AESCryptor interface {\n\tCBCEncrypt(data []byte) ([]byte, error)\n\tCBCDecrypt(cipher []byte) ([]byte, error)\n}\n\ntype aesCryptor struct {\n\tkey []byte\n}\n\nfunc (c *aesCryptor) CBCEncrypt(data []byte) ([]byte, error) {\n\tblock, err := aes.NewCipher(c.key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpaddedData := c.pkcs7Padding(data, aes.BlockSize)\n\tciphertext := make([]byte, aes.BlockSize+len(paddedData))\n\tiv := ciphertext[:aes.BlockSize]\n\tif _, err := io.ReadFull(rand.Reader, iv); err != nil {\n\t\treturn nil, err\n\t}\n\n\tmode := cipher.NewCBCEncrypter(block, iv)\n\tmode.CryptBlocks(ciphertext[aes.BlockSize:], paddedData)\n\n\treturn ciphertext, nil\n}\n\nfunc (c *aesCryptor) CBCDecrypt(ciphertext []byte) ([]byte, error) {\n\tblock, err := aes.NewCipher(c.key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(ciphertext) < aes.BlockSize {\n\t\treturn nil, fmt.Errorf(\"ciphertext too short\")\n\t}\n\n\tiv := ciphertext[:aes.BlockSize]\n\tciphertext = ciphertext[aes.BlockSize:]\n\n\tif len(ciphertext)%aes.BlockSize != 0 {\n\t\treturn nil, fmt.Errorf(\"ciphertext is not a multiple of the block size\")\n\t}\n\n\tmode := cipher.NewCBCDecrypter(block, iv)\n\tmode.CryptBlocks(ciphertext, ciphertext)\n\n\treturn c.pkcs7Unpadding(ciphertext), nil\n}\n\nfunc (c *aesCryptor) pkcs7Padding(data []byte, blockSize int) []byte {\n\tpadding := blockSize - (len(data) % blockSize)\n\tpadText := bytes.Repeat([]byte{byte(padding)}, padding)\n\treturn append(data, padText...)\n}\n\nfunc (c *aesCryptor) pkcs7Unpadding(data []byte) []byte {\n\tlength := len(data)\n\tif length == 0 {\n\t\treturn data\n\t}\n\n\tpadding := int(data[length-1])\n\tif padding > length {\n\t\treturn data\n\t}\n\n\treturn data[:length-padding]\n}\n\nfunc NewAESCryptor(key []byte) AESCryptor {\n\treturn &aesCryptor{key: key}\n}\n"
  },
  {
    "path": "pkg/utils/env/get.go",
    "content": "package env\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"strconv\"\n)\n\n// 以字符串形式读取指定环境变量的值。\n//\n// 入参：\n//   - envVar：环境变量。\n//\n// 出参：\n//   - 环境变量值。\nfunc GetString(envVar string) string {\n\treturn GetOrDefaultString(envVar, \"\")\n}\n\n// 以字符串形式读取指定环境变量的值。\n//\n// 入参：\n//   - envVar：环境变量。\n//   - defaultValue: 默认值。\n//\n// 出参：\n//   - 环境变量值。如果指定环境变量不存在、或者值为零值，则返回默认值。\nfunc GetOrDefaultString(envVar, defaultValue string) string {\n\treturn getOrDefault(envVar, defaultValue, parseString)\n}\n\n// 以整数形式读取指定环境变量的值。\n//\n// 入参：\n//   - envVar：环境变量。\n//\n// 出参：\n//   - 环境变量值。\nfunc GetInt(envVar string) int {\n\treturn GetOrDefaultInt(envVar, 0)\n}\n\n// 以整数形式读取指定环境变量的值。\n//\n// 入参：\n//   - envVar：环境变量。\n//   - defaultValue: 默认值。\n//\n// 出参：\n//   - 环境变量值。如果指定环境变量不存在、或者值的类型不是整数，则返回默认值。\nfunc GetOrDefaultInt(envVar string, defaultValue int) int {\n\treturn getOrDefault(envVar, defaultValue, strconv.Atoi)\n}\n\n// 以布尔形式读取指定环境变量的值。\n//\n// 入参：\n//   - envVar：环境变量。\n//\n// 出参：\n//   - 环境变量值。\nfunc GetBool(envVar string) bool {\n\treturn GetOrDefaultBool(envVar, false)\n}\n\n// 以布尔形式读取指定环境变量的值。\n//\n// 入参：\n//   - envVar：环境变量。\n//   - defaultValue: 默认值。\n//\n// 出参：\n//   - 环境变量值。如果指定环境变量不存在、或者值的类型不是布尔，则返回默认值。\nfunc GetOrDefaultBool(envVar string, defaultValue bool) bool {\n\treturn getOrDefault(envVar, defaultValue, strconv.ParseBool)\n}\n\nfunc getOrDefault[T any](envVar string, defaultValue T, parser func(string) (T, error)) T {\n\tv, err := parser(os.Getenv(envVar))\n\tif err != nil {\n\t\treturn defaultValue\n\t}\n\n\treturn v\n}\n\nfunc parseString(s string) (string, error) {\n\tif s == \"\" {\n\t\treturn \"\", errors.New(\"empty string\")\n\t}\n\n\treturn s, nil\n}\n"
  },
  {
    "path": "pkg/utils/file/io.go",
    "content": "package file\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\n// 与 [Write] 类似，但写入的是字符串内容。\n//\n// 入参:\n//   - path: 文件路径。\n//   - content: 文件内容。\n//\n// 出参:\n//   - 错误。\nfunc WriteString(path string, content string) error {\n\treturn Write(path, []byte(content))\n}\n\n// 将数据写入指定路径的文件。\n// 如果目录不存在，将会递归创建目录。\n// 如果文件不存在，将会创建该文件；如果文件已存在，将会覆盖原有内容。\n//\n// 入参:\n//   - path: 文件路径。\n//   - data: 文件数据字节数组。\n//\n// 出参:\n//   - 错误。\nfunc Write(path string, data []byte) error {\n\tdir := filepath.Dir(path)\n\n\terr := os.MkdirAll(dir, os.ModePerm)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create directory: %w\", err)\n\t}\n\n\tfile, err := os.Create(path)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create file: %w\", err)\n\t}\n\tdefer file.Close()\n\n\t_, err = file.Write(data)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write file: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/utils/filepath/path.go",
    "content": "package filepath\n\nimport (\n\tstdfilepath \"path/filepath\"\n\t\"strings\"\n)\n\n// 与标准库中的 [filepath.Dir] 类似，但会尝试保留原有的路径分隔符。\n//\n// 入参:\n//   - path: 文件路径。\n//\n// 出参:\n//   - 目录路径。\nfunc Dir(path string) string {\n\tconst SEP_WIN = \"\\\\\"\n\tconst SEP_UNIX = \"/\"\n\n\tsep := SEP_UNIX\n\tif strings.Contains(path, SEP_WIN) && !strings.Contains(path, SEP_UNIX) {\n\t\tsep = SEP_WIN\n\t}\n\n\tdir := stdfilepath.Dir(path)\n\n\tif sep != SEP_UNIX && strings.Contains(dir, SEP_UNIX) {\n\t\tdir = strings.ReplaceAll(dir, SEP_UNIX, sep)\n\t} else if sep != SEP_WIN && strings.Contains(dir, SEP_WIN) {\n\t\tdir = strings.ReplaceAll(dir, SEP_WIN, sep)\n\t}\n\n\treturn dir\n}\n"
  },
  {
    "path": "pkg/utils/http/parser.go",
    "content": "package http\n\nimport (\n\t\"bufio\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"strings\"\n)\n\n// 从表示 HTTP 标头的字符串解析并返回一个 http.Header 对象。\n//\n// 入参:\n//   - headers: 表示 HTTP 标头的字符串。\n//\n// 出参:\n//   - header: http.Header 对象。\n//   - err: 错误。\nfunc ParseHeaders(headers string) (http.Header, error) {\n\tstr := strings.TrimSpace(headers) + \"\\r\\n\\r\\n\"\n\tif len(str) == 4 {\n\t\treturn make(http.Header), nil\n\t}\n\n\tbr := bufio.NewReader(strings.NewReader(str))\n\ttp := textproto.NewReader(br)\n\n\tmimeHeader, err := tp.ReadMIMEHeader()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn http.Header(mimeHeader), err\n}\n"
  },
  {
    "path": "pkg/utils/http/transport.go",
    "content": "package http\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n)\n\n// 创建并返回一个 [http.DefaultTransport] 对象副本。\n//\n// 出参：\n//   - transport: [http.DefaultTransport] 对象副本。\nfunc NewDefaultTransport() *http.Transport {\n\tif http.DefaultTransport != nil {\n\t\tif t, ok := http.DefaultTransport.(*http.Transport); ok {\n\t\t\treturn t.Clone()\n\t\t}\n\t}\n\n\treturn &http.Transport{\n\t\tProxy: http.ProxyFromEnvironment,\n\t\tDialContext: (&net.Dialer{\n\t\t\tTimeout:   30 * time.Second,\n\t\t\tKeepAlive: 30 * time.Second,\n\t\t\tDualStack: true,\n\t\t}).DialContext,\n\t\tForceAttemptHTTP2:     true,\n\t\tMaxIdleConns:          100,\n\t\tIdleConnTimeout:       90 * time.Second,\n\t\tTLSHandshakeTimeout:   10 * time.Second,\n\t\tExpectContinueTimeout: 1 * time.Second,\n\t}\n}\n"
  },
  {
    "path": "pkg/utils/maps/get.go",
    "content": "package maps\n\nimport (\n\t\"strconv\"\n)\n\n// 以字符串形式从字典中获取指定键的值。\n//\n// 入参：\n//   - dict: 字典。\n//   - key: 键。\n//\n// 出参：\n//   - 字典中键对应的值。如果指定键不存在、或者值的类型不是字符串，则返回空字符串。\nfunc GetString(dict map[string]any, key string) string {\n\treturn GetOrDefaultString(dict, key, \"\")\n}\n\n// 以字符串形式从字典中获取指定键的值。\n//\n// 入参：\n//   - dict: 字典。\n//   - key: 键。\n//   - defaultValue: 默认值。\n//\n// 出参：\n//   - 字典中键对应的值。如果指定键不存在、值的类型不是字符串、或者值为零值，则返回默认值。\nfunc GetOrDefaultString(dict map[string]any, key string, defaultValue string) string {\n\tif dict == nil {\n\t\treturn defaultValue\n\t}\n\n\tif value, ok := dict[key]; ok {\n\t\tif result, ok := value.(string); ok {\n\t\t\tif result != \"\" {\n\t\t\t\treturn result\n\t\t\t}\n\t\t}\n\t}\n\n\treturn defaultValue\n}\n\n// 以整数形式从字典中获取指定键的值。\n//\n// 入参：\n//   - dict: 字典。\n//   - key: 键。\n//\n// 出参：\n//   - 字典中键对应的值。如果指定键不存在、或者值的类型不是整数，则返回 0。\nfunc GetInt(dict map[string]any, key string) int {\n\treturn GetOrDefaultInt(dict, key, 0)\n}\n\n// 以整数形式从字典中获取指定键的值。\n//\n// 入参：\n//   - dict: 字典。\n//   - key: 键。\n//   - defaultValue: 默认值。\n//\n// 出参：\n//   - 字典中键对应的值。如果指定键不存在、值的类型不是整数、或者值为零值，则返回默认值。\nfunc GetOrDefaultInt(dict map[string]any, key string, defaultValue int) int {\n\tif dict == nil {\n\t\treturn defaultValue\n\t}\n\n\tif value, ok := dict[key]; ok {\n\t\tvar result int\n\n\t\tswitch v := value.(type) {\n\t\tcase int:\n\t\t\tresult = v\n\t\tcase int8:\n\t\t\tresult = int(v)\n\t\tcase int16:\n\t\t\tresult = int(v)\n\t\tcase int32:\n\t\t\tresult = int(v)\n\t\tcase int64:\n\t\t\tresult = int(v)\n\t\tcase uint:\n\t\t\tresult = int(v)\n\t\tcase uint8:\n\t\t\tresult = int(v)\n\t\tcase uint16:\n\t\t\tresult = int(v)\n\t\tcase uint32:\n\t\t\tresult = int(v)\n\t\tcase uint64:\n\t\t\tresult = int(v)\n\t\tcase float32:\n\t\t\tresult = int(v)\n\t\tcase float64:\n\t\t\tresult = int(v)\n\t\tcase string:\n\t\t\t// 兼容字符串类型的值\n\t\t\tif t, err := strconv.ParseInt(v, 10, 64); err == nil {\n\t\t\t\tresult = int(t)\n\t\t\t}\n\t\t}\n\n\t\tif result != 0 {\n\t\t\treturn result\n\t\t}\n\t}\n\n\treturn defaultValue\n}\n\n// 以 32 位整数形式从字典中获取指定键的值。\n//\n// 入参：\n//   - dict: 字典。\n//   - key: 键。\n//\n// 出参：\n//   - 字典中键对应的值。如果指定键不存在、或者值的类型不是 32 位整数，则返回 0。\nfunc GetInt32(dict map[string]any, key string) int32 {\n\treturn GetOrDefaultInt32(dict, key, 0)\n}\n\n// 以 32 位整数形式从字典中获取指定键的值。\n//\n// 入参：\n//   - dict: 字典。\n//   - key: 键。\n//   - defaultValue: 默认值。\n//\n// 出参：\n//   - 字典中键对应的值。如果指定键不存在、值的类型不是 32 位整数、或者值为零值，则返回默认值。\nfunc GetOrDefaultInt32(dict map[string]any, key string, defaultValue int32) int32 {\n\tif dict == nil {\n\t\treturn defaultValue\n\t}\n\n\tif value, ok := dict[key]; ok {\n\t\tvar result int32\n\n\t\tswitch v := value.(type) {\n\t\tcase int:\n\t\t\tresult = int32(v)\n\t\tcase int8:\n\t\t\tresult = int32(v)\n\t\tcase int16:\n\t\t\tresult = int32(v)\n\t\tcase int32:\n\t\t\tresult = v\n\t\tcase int64:\n\t\t\tresult = int32(v)\n\t\tcase uint:\n\t\t\tresult = int32(v)\n\t\tcase uint8:\n\t\t\tresult = int32(v)\n\t\tcase uint16:\n\t\t\tresult = int32(v)\n\t\tcase uint32:\n\t\t\tresult = int32(v)\n\t\tcase uint64:\n\t\t\tresult = int32(v)\n\t\tcase float32:\n\t\t\tresult = int32(v)\n\t\tcase float64:\n\t\t\tresult = int32(v)\n\t\tcase string:\n\t\t\t// 兼容字符串类型的值\n\t\t\tif t, err := strconv.ParseInt(v, 10, 32); err == nil {\n\t\t\t\tresult = int32(t)\n\t\t\t}\n\t\t}\n\n\t\tif result != 0 {\n\t\t\treturn result\n\t\t}\n\t}\n\n\treturn defaultValue\n}\n\n// 以 64 位整数形式从字典中获取指定键的值。\n//\n// 入参：\n//   - dict: 字典。\n//   - key: 键。\n//\n// 出参：\n//   - 字典中键对应的值。如果指定键不存在、或者值的类型不是 64 位整数，则返回 0。\nfunc GetInt64(dict map[string]any, key string) int64 {\n\treturn GetOrDefaultInt64(dict, key, 0)\n}\n\n// 以 64 位整数形式从字典中获取指定键的值。\n//\n// 入参：\n//   - dict: 字典。\n//   - key: 键。\n//   - defaultValue: 默认值。\n//\n// 出参：\n//   - 字典中键对应的值。如果指定键不存在、值的类型不是 64 位整数、或者值为零值，则返回默认值。\nfunc GetOrDefaultInt64(dict map[string]any, key string, defaultValue int64) int64 {\n\tif dict == nil {\n\t\treturn defaultValue\n\t}\n\n\tif value, ok := dict[key]; ok {\n\t\tvar result int64\n\n\t\tswitch v := value.(type) {\n\t\tcase int:\n\t\t\tresult = int64(v)\n\t\tcase int8:\n\t\t\tresult = int64(v)\n\t\tcase int16:\n\t\t\tresult = int64(v)\n\t\tcase int32:\n\t\t\tresult = int64(v)\n\t\tcase int64:\n\t\t\tresult = v\n\t\tcase uint:\n\t\t\tresult = int64(v)\n\t\tcase uint8:\n\t\t\tresult = int64(v)\n\t\tcase uint16:\n\t\t\tresult = int64(v)\n\t\tcase uint32:\n\t\t\tresult = int64(v)\n\t\tcase uint64:\n\t\t\tresult = int64(v)\n\t\tcase float32:\n\t\t\tresult = int64(v)\n\t\tcase float64:\n\t\t\tresult = int64(v)\n\t\tcase string:\n\t\t\t// 兼容字符串类型的值\n\t\t\tif t, err := strconv.ParseInt(v, 10, 64); err == nil {\n\t\t\t\tresult = t\n\t\t\t}\n\t\t}\n\n\t\tif result != 0 {\n\t\t\treturn result\n\t\t}\n\t}\n\n\treturn defaultValue\n}\n\n// 以布尔形式从字典中获取指定键的值。\n//\n// 入参：\n//   - dict: 字典。\n//   - key: 键。\n//\n// 出参：\n//   - 字典中键对应的值。如果指定键不存在、或者值的类型不是布尔，则返回 false。\nfunc GetBool(dict map[string]any, key string) bool {\n\treturn GetOrDefaultBool(dict, key, false)\n}\n\n// 以布尔形式从字典中获取指定键的值。\n//\n// 入参：\n//   - dict: 字典。\n//   - key: 键。\n//   - defaultValue: 默认值。\n//\n// 出参：\n//   - 字典中键对应的值。如果指定键不存在、或者值的类型不是布尔，则返回默认值。\nfunc GetOrDefaultBool(dict map[string]any, key string, defaultValue bool) bool {\n\tif dict == nil {\n\t\treturn defaultValue\n\t}\n\n\tif value, ok := dict[key]; ok {\n\t\tif result, ok := value.(bool); ok {\n\t\t\treturn result\n\t\t}\n\n\t\t// 兼容字符串类型的值\n\t\tif str, ok := value.(string); ok {\n\t\t\tif result, err := strconv.ParseBool(str); err == nil {\n\t\t\t\treturn result\n\t\t\t}\n\t\t}\n\t}\n\n\treturn defaultValue\n}\n\n// 以 `map[string]V` 形式从字典中获取指定键的值。\n//\n// 入参：\n//   - dict: 字典。\n//   - key: 键。\n//\n// 出参：\n//   - 字典中键对应的 `map[string]V` 对象。\nfunc GetKVMap[V any](dict map[string]any, key string) map[string]V {\n\tif dict == nil {\n\t\treturn make(map[string]V)\n\t}\n\n\tif val, ok := dict[key]; ok {\n\t\tif result, ok := val.(map[string]V); ok {\n\t\t\treturn result\n\t\t}\n\t}\n\n\treturn make(map[string]V)\n}\n\n// 以 `map[string]any` 形式从字典中获取指定键的值。\n//\n// 入参：\n//   - dict: 字典。\n//   - key: 键。\n//\n// 出参：\n//   - 字典中键对应的 `map[string]any` 对象。\nfunc GetKVMapAny(dict map[string]any, key string) map[string]any {\n\treturn GetKVMap[any](dict, key)\n}\n"
  },
  {
    "path": "pkg/utils/maps/marshal.go",
    "content": "package maps\n\nimport (\n\tmapstructure \"github.com/go-viper/mapstructure/v2\"\n)\n\n// 将字典填充到指定类型的结构体。\n// 与 [json.Unmarshal] 类似，但传入的是一个 [map[string]any] 对象而非 JSON 格式的字符串。\n//\n// 入参：\n//   - dict: 字典。\n//   - output: 结构体指针。\n//\n// 出参：\n//   - 错误信息。如果填充失败，则返回错误信息。\nfunc Populate(dict map[string]any, output any) error {\n\tconfig := &mapstructure.DecoderConfig{\n\t\tMetadata:         nil,\n\t\tResult:           output,\n\t\tWeaklyTypedInput: true,\n\t\tTagName:          \"json\",\n\t}\n\n\tdecoder, err := mapstructure.NewDecoder(config)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn decoder.Decode(dict)\n}\n"
  },
  {
    "path": "pkg/utils/ssh/cmd.go",
    "content": "package ssh\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// 执行远程脚本命令，并返回执行后标准输出和标准错误。\n//\n// 入参:\n//   - sshCli: SSH 客户端。\n//   - command: 待执行的脚本命令。\n//\n// 出参:\n//   - stdout：标准输出。\n//   - stderr：标准错误。\n//   - err: 错误。\nfunc RunCommand(sshCli *ssh.Client, command string) (string, string, error) {\n\tsession, err := sshCli.NewSession()\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tdefer session.Close()\n\n\tstdoutBuf := bytes.NewBuffer(nil)\n\tsession.Stdout = stdoutBuf\n\tstderrBuf := bytes.NewBuffer(nil)\n\tsession.Stderr = stderrBuf\n\terr = session.Run(command)\n\tif err != nil {\n\t\treturn stdoutBuf.String(), stderrBuf.String(), fmt.Errorf(\"failed to execute ssh command: %w\", err)\n\t}\n\n\treturn stdoutBuf.String(), stderrBuf.String(), nil\n}\n"
  },
  {
    "path": "pkg/utils/ssh/io.go",
    "content": "package ssh\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/pkg/sftp\"\n\t\"github.com/povsister/scp\"\n\t\"golang.org/x/crypto/ssh\"\n\n\txfilepath \"github.com/certimate-go/certimate/pkg/utils/filepath\"\n)\n\n// 与 [WriteRemote] 类似，但写入的是字符串内容。\n//\n// 入参:\n//   - sshCli: SSH 客户端。\n//   - path: 文件远程路径。\n//   - data: 文件数据字节数组。\n//   - useSCP: 是否使用 SCP 进行传输，否则使用 SFTP。\n//\n// 出参:\n//   - 错误。\nfunc WriteRemoteString(sshCli *ssh.Client, path string, content string, useSCP bool) error {\n\tif useSCP {\n\t\treturn writeRemoteStringWithSCP(sshCli, path, content)\n\t}\n\n\treturn writeRemoteStringWithSFTP(sshCli, path, content)\n}\n\n// 将数据写入指定远程路径的文件。\n// 如果目录不存在，将会递归创建目录。\n// 如果文件不存在，将会创建该文件；如果文件已存在，将会覆盖原有内容。\n//\n// 入参:\n//   - sshCli: SSH 客户端。\n//   - path: 文件远程路径。\n//   - data: 文件数据字节数组。\n//   - useSCP: 是否使用 SCP 进行传输，否则使用 SFTP。\n//\n// 出参:\n//   - 错误。\nfunc WriteRemote(sshCli *ssh.Client, path string, data []byte, useSCP bool) error {\n\tif useSCP {\n\t\treturn writeRemoteWithSCP(sshCli, path, data)\n\t}\n\n\treturn writeRemoteWithSFTP(sshCli, path, data)\n}\n\n// 删除指定远程路径的文件。\n//\n// 入参:\n//   - sshCli: SSH 客户端。\n//   - path: 文件远程路径。\n//   - useSCP: 是否使用 SCP 进行传输，否则使用 SFTP。\n//\n// 出参:\n//   - 错误。\nfunc RemoveRemote(sshCli *ssh.Client, path string, useSCP bool) error {\n\tif useSCP {\n\t\treturn errors.ErrUnsupported\n\t}\n\n\treturn removeRemoteWithSFTP(sshCli, path)\n}\n\nfunc writeRemoteStringWithSCP(sshCli *ssh.Client, path string, content string) error {\n\treturn writeRemoteWithSCP(sshCli, path, []byte(content))\n}\n\nfunc writeRemoteStringWithSFTP(sshCli *ssh.Client, path string, content string) error {\n\treturn writeRemoteWithSFTP(sshCli, path, []byte(content))\n}\n\nfunc writeRemoteWithSCP(sshCli *ssh.Client, path string, data []byte) error {\n\tscpCli, err := scp.NewClientFromExistingSSH(sshCli, &scp.ClientOption{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create scp client: %w\", err)\n\t}\n\n\treader := bytes.NewReader(data)\n\terr = scpCli.CopyToRemote(reader, path, &scp.FileTransferOption{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write to remote file: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc writeRemoteWithSFTP(sshCli *ssh.Client, path string, data []byte) error {\n\tsftpCli, err := sftp.NewClient(sshCli)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create sftp client: %w\", err)\n\t}\n\tdefer sftpCli.Close()\n\n\tif err := sftpCli.MkdirAll(xfilepath.Dir(path)); err != nil {\n\t\treturn fmt.Errorf(\"failed to create remote directory: %w\", err)\n\t}\n\n\tfile, err := sftpCli.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open remote file: %w\", err)\n\t}\n\tdefer file.Close()\n\n\t_, err = file.Write(data)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write to remote file: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc removeRemoteWithSFTP(sshCli *ssh.Client, path string) error {\n\tsftpCli, err := sftp.NewClient(sshCli)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create sftp client: %w\", err)\n\t}\n\tdefer sftpCli.Close()\n\n\tif err := sftpCli.MkdirAll(xfilepath.Dir(path)); err != nil {\n\t\treturn fmt.Errorf(\"failed to create remote directory: %w\", err)\n\t}\n\n\tif err := sftpCli.Remove(path); err != nil {\n\t\treturn fmt.Errorf(\"failed to remove remote file: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/utils/tls/config.go",
    "content": "package tls\n\nimport (\n\t\"crypto/tls\"\n)\n\n// 创建并返回一个兼容低版的 [tls.Config] 对象。\n//\n// 出参：\n//   - config: [tls.Config] 对象。\nfunc NewCompatibleConfig() *tls.Config {\n\tvar suiteIds []uint16\n\tfor _, suite := range tls.CipherSuites() {\n\t\tsuiteIds = append(suiteIds, suite.ID)\n\t}\n\tfor _, suite := range tls.InsecureCipherSuites() {\n\t\tsuiteIds = append(suiteIds, suite.ID)\n\t}\n\n\treturn &tls.Config{\n\t\tMinVersion:   tls.VersionTLS10,\n\t\tCipherSuites: suiteIds,\n\t}\n}\n\n// 创建并返回一个不安全的 [tls.Config] 对象。\n//\n// 出参：\n//   - config: [tls.Config] 对象。\nfunc NewInsecureConfig() *tls.Config {\n\tconfig := NewCompatibleConfig()\n\tconfig.InsecureSkipVerify = true\n\treturn config\n}\n"
  },
  {
    "path": "pkg/utils/wait/delay.go",
    "content": "﻿package wait\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n// 等待一段时间。\n//\n// 入参：\n//   - wait: 等待时间。\n//\n// 出参：\n//   - err: 错误。\nfunc Delay(wait time.Duration) error {\n\treturn DelayWithContext(context.Background(), wait)\n}\n\n// 等待一段时间，或上下文被取消。\n//\n// 入参：\n//   - ctx: 上下文。\n//   - wait: 等待时间。\n//\n// 出参：\n//   - err: 错误。\nfunc DelayWithContext(ctx context.Context, wait time.Duration) error {\n\tticker := time.NewTimer(wait)\n\tdefer ticker.Stop()\n\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-ticker.C:\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "pkg/utils/wait/until.go",
    "content": "﻿package wait\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n// 等待直到条件满足。\n//\n// 入参：\n//   - condition: 条件函数，接收尝试次数作为参数，返回是否满足条件和错误。\n//   - interval: 执行条件函数的间隔时间。\n//\n// 出参：\n//   - ret: 是否满足条件。\n//   - err: 错误。\nfunc Until(condition func(index int) (bool, error), interval time.Duration) (bool, error) {\n\tconditionWithContext := func(_ context.Context, index int) (bool, error) {\n\t\treturn condition(index)\n\t}\n\treturn UntilWithContext(context.Background(), conditionWithContext, interval)\n}\n\n// 等待直到条件满足，或上下文被取消。\n//\n// 入参：\n//   - ctx: 上下文。\n//   - condition: 条件函数，接收上下文和尝试次数作为参数，返回是否满足条件和错误。\n//   - interval: 执行条件函数的间隔时间。\n//\n// 出参：\n//   - ret: 是否满足条件。\n//   - err: 错误。\nfunc UntilWithContext(ctx context.Context, condition func(ctx context.Context, index int) (bool, error), interval time.Duration) (bool, error) {\n\tticker := time.NewTicker(interval)\n\tdefer ticker.Stop()\n\n\tattempt := 0\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn false, ctx.Err()\n\n\t\tcase <-ticker.C:\n\t\t\tattempt++\n\t\t\tret, err := condition(ctx, attempt)\n\t\t\tif ret || err != nil {\n\t\t\t\treturn ret, err\n\t\t\t}\n\t\t}\n\t}\n}\n\n// 等待直到条件满足或超时。\n//\n// 入参：\n//   - condition: 条件函数，接收尝试次数作为参数，返回是否满足条件和错误。\n//   - timeout: 超时时间。\n//   - interval: 执行条件函数的间隔时间。\n//\n// 出参：\n//   - ret: 是否满足条件。\n//   - err: 错误。\nfunc UntilTimeout(condition func(index int) (bool, error), timeout time.Duration, interval time.Duration) (bool, error) {\n\tconditionWithContext := func(_ context.Context, index int) (bool, error) {\n\t\treturn condition(index)\n\t}\n\treturn UntilTimeoutWithContext(context.Background(), conditionWithContext, timeout, interval)\n}\n\n// 等待直到条件满足或超时，或上下文被取消。\n//\n// 入参：\n//   - ctx: 上下文。\n//   - condition: 条件函数，接收上下文和尝试次数作为参数，返回是否满足条件和错误。\n//   - timeout: 超时时间。\n//   - interval: 执行条件函数的间隔时间。\n//\n// 出参：\n//   - ret: 是否满足条件。\n//   - err: 错误。\nfunc UntilTimeoutWithContext(ctx context.Context, condition func(ctx context.Context, index int) (bool, error), timeout time.Duration, interval time.Duration) (bool, error) {\n\tctxWithTimeout, cancel := context.WithTimeout(ctx, timeout)\n\tdefer cancel()\n\n\tticker := time.NewTicker(interval)\n\tdefer ticker.Stop()\n\n\tattempt := 0\n\tfor {\n\t\tselect {\n\t\tcase <-ctxWithTimeout.Done():\n\t\t\treturn false, ctx.Err()\n\n\t\tcase <-ticker.C:\n\t\t\tattempt++\n\t\t\tret, err := condition(ctxWithTimeout, attempt)\n\t\t\tif ret || err != nil {\n\t\t\t\treturn ret, err\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "ui/.gitignore",
    "content": "node_modules\ndist\ndist-ssr\n!dist/.gitkeep\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n*.local\n.env\n"
  },
  {
    "path": "ui/embed.go",
    "content": "// Package ui handles the PocketBase Admin frontend embedding.\npackage ui\n\nimport (\n\t\"embed\"\n\n\t\"github.com/pocketbase/pocketbase/apis\"\n)\n\n//go:embed all:dist\nvar distDir embed.FS\n\n// DistDirFS contains the embedded dist directory files (without the \"dist\" prefix)\nvar DistDirFS = apis.MustSubFS(distDir, \"dist\")\n"
  },
  {
    "path": "ui/eslint.config.mjs",
    "content": "import eslint from \"@eslint/js\";\nimport { defineConfig } from \"eslint/config\";\nimport tailwindcssPlugin from \"eslint-plugin-better-tailwindcss\";\nimport importPlugin from \"eslint-plugin-import\";\nimport prettierPluginConfig from \"eslint-plugin-prettier/recommended\";\nimport reactHooksPlugin from \"eslint-plugin-react-hooks\";\nimport reactRefreshPlugin from \"eslint-plugin-react-refresh\";\nimport typescriptPlugin from \"typescript-eslint\";\n\n/**\n * @type {import(\"eslint\").Linter.Config[]}\n */\nexport default defineConfig(\n  // Basic\n  eslint.configs[\"recommended\"],\n  {\n    name: \"eslint/import\",\n    extends: [importPlugin.flatConfigs[\"recommended\"], importPlugin.flatConfigs[\"typescript\"]],\n    rules: {\n      \"import/no-named-as-default-member\": \"off\",\n      \"import/no-unresolved\": \"off\",\n      \"import/order\": [\n        \"error\",\n        {\n          groups: [\"builtin\", \"external\", \"internal\", [\"parent\", \"sibling\"], \"index\"],\n          pathGroups: [\n            {\n              pattern: \"react*\",\n              group: \"external\",\n              position: \"before\",\n            },\n            {\n              pattern: \"react/**\",\n              group: \"external\",\n              position: \"before\",\n            },\n            {\n              pattern: \"react-*\",\n              group: \"external\",\n              position: \"before\",\n            },\n            {\n              pattern: \"react-*/**\",\n              group: \"external\",\n              position: \"before\",\n            },\n            {\n              pattern: \"~/**\",\n              group: \"external\",\n              position: \"after\",\n            },\n            {\n              pattern: \"@/**\",\n              group: \"internal\",\n              position: \"before\",\n            },\n          ],\n          pathGroupsExcludedImportTypes: [\"builtin\"],\n          alphabetize: {\n            order: \"asc\",\n            caseInsensitive: true,\n          },\n        },\n      ],\n      \"sort-imports\": [\n        \"error\",\n        {\n          ignoreDeclarationSort: true,\n        },\n      ],\n    },\n    settings: {\n      \"import/resolver\": {\n        node: {\n          extensions: [\".js\", \".jsx\", \".ts\", \".tsx\"],\n        },\n        typescript: {\n          alwaysTryTypes: true,\n        },\n      },\n    },\n  },\n\n  // Typescript\n  {\n    name: \"typescript\",\n    extends: [typescriptPlugin.configs[\"recommended\"]],\n    rules: {\n      \"@typescript-eslint/consistent-type-imports\": \"error\",\n      \"@typescript-eslint/no-empty-object-type\": [\n        \"error\",\n        {\n          allowInterfaces: \"with-single-extends\",\n        },\n      ],\n      \"@typescript-eslint/no-explicit-any\": [\n        \"warn\",\n        {\n          ignoreRestArgs: true,\n        },\n      ],\n      \"@typescript-eslint/no-unused-vars\": [\n        \"error\",\n        {\n          argsIgnorePattern: \"^_\",\n          caughtErrorsIgnorePattern: \"^_\",\n          destructuredArrayIgnorePattern: \"^_\",\n          varsIgnorePattern: \"^_\",\n        },\n      ],\n    },\n  },\n\n  // Pretter\n  {\n    name: \"prettier\",\n    extends: [prettierPluginConfig],\n  },\n\n  // React\n  {\n    name: \"react\",\n    extends: [reactHooksPlugin.configs.flat[\"recommended-latest\"], reactRefreshPlugin.configs[\"vite\"]],\n    rules: {\n      \"react-hooks/exhaustive-deps\": [\"warn\"],\n      \"react-hooks/immutability\": [\"warn\"],\n      \"react-hooks/refs\": [\"warn\"],\n      \"react-hooks/preserve-manual-memoization\": [\"off\"],\n      \"react-hooks/set-state-in-effect\": [\"warn\"],\n      \"react-hooks/set-state-in-render\": [\"warn\"],\n      \"react-refresh/only-export-components\": [\n        \"warn\",\n        {\n          allowConstantExport: true,\n        },\n      ],\n    },\n  },\n\n  // TailwindCSS\n  {\n    name: \"tailwindcss\",\n    plugins: {\n      \"better-tailwindcss\": tailwindcssPlugin,\n    },\n    rules: {\n      ...tailwindcssPlugin.configs[\"recommended-warn\"].rules,\n      ...tailwindcssPlugin.configs[\"recommended-error\"].rules,\n\n      \"better-tailwindcss/enforce-consistent-line-wrapping\": \"off\",\n      \"better-tailwindcss/no-unknown-classes\": \"off\",\n    },\n    settings: {\n      \"better-tailwindcss\": {\n        entryPoint: \"src/global.css\",\n      },\n    },\n  }\n);\n"
  },
  {
    "path": "ui/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/logo.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta name=\"robots\" content=\"noindex\" />\n    <title>Certimate - Your Trusted Partner in SSL Automation</title>\n  </head>\n  <body style=\"pointer-events: auto !important\">\n    <div id=\"root\">\n      <div class=\"apploader\"></div>\n      <style>\n        .apploader {\n          position: fixed;\n          top: 50%;\n          left: 50%;\n          width: 50px;\n          height: 50px;\n          border-radius: 50px;\n          background: var(--color-primary, orange);\n          -webkit-transform: translate(-50%, -50%);\n          -moz-transform: translate(-50%, -50%);\n          transform: translate(-50%, -50%);\n          -webkit-animation: apploader infinite linear 0.75s;\n          -moz-animation: apploader infinite linear 0.75s;\n          animation: apploader infinite linear 0.75s;\n        }\n\n        @-webkit-keyframes apploader {\n          0% {\n            opacity: 1;\n            -webkit-transform: translate(-50%, -50%) scale(0.1);\n          }\n          100% {\n            opacity: 0;\n            -webkit-transform: translate(-50%, -50%) scale(1);\n          }\n        }\n\n        @-moz-keyframes apploader {\n          0% {\n            opacity: 1;\n            -moz-transform: translate(-50%, -50%) scale(0.1);\n          }\n          100% {\n            opacity: 0;\n            -moz-transform: translate(-50%, -50%) scale(1);\n          }\n        }\n\n        @keyframes apploader {\n          0% {\n            opacity: 1;\n            transform: translate(-50%, -50%) scale(0.1);\n          }\n          100% {\n            opacity: 0;\n            transform: translate(-50%, -50%) scale(1);\n          }\n        }\n      </style>\n    </div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "ui/package.json",
    "content": "{\n  \"name\": \"@certimate/webui\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite --host\",\n    \"build\": \"tsc -b && vite build\",\n    \"lint\": \"eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@codemirror/lang-json\": \"^6.0.2\",\n    \"@codemirror/lang-yaml\": \"^6.1.2\",\n    \"@codemirror/language\": \"^6.12.2\",\n    \"@codemirror/legacy-modes\": \"^6.5.2\",\n    \"@flowgram.ai/document\": \"1.0.8\",\n    \"@flowgram.ai/fixed-layout-editor\": \"1.0.8\",\n    \"@flowgram.ai/minimap-plugin\": \"1.0.8\",\n    \"@peculiar/asn1-ecc\": \"^2.6.1\",\n    \"@peculiar/asn1-pkcs8\": \"^2.6.1\",\n    \"@peculiar/asn1-rsa\": \"^2.6.1\",\n    \"@peculiar/asn1-schema\": \"^2.6.0\",\n    \"@peculiar/x509\": \"^1.14.3\",\n    \"@tabler/icons-react\": \"^3.40.0\",\n    \"@uiw/codemirror-extensions-basic-setup\": \"^4.25.8\",\n    \"@uiw/codemirror-theme-vscode\": \"^4.25.8\",\n    \"@uiw/react-codemirror\": \"^4.25.8\",\n    \"ahooks\": \"^3.9.6\",\n    \"antd\": \"^6.3.2\",\n    \"antd-zod\": \"^8.0.0\",\n    \"clsx\": \"^2.1.1\",\n    \"cron-parser\": \"^5.5.0\",\n    \"dayjs\": \"^1.11.20\",\n    \"file-saver\": \"^2.0.5\",\n    \"i18next\": \"^25.8.18\",\n    \"i18next-browser-languagedetector\": \"^8.2.1\",\n    \"immer\": \"^11.1.4\",\n    \"nanoid\": \"^5.1.7\",\n    \"pocketbase\": \"^0.26.8\",\n    \"radash\": \"^12.1.1\",\n    \"react\": \"^18.3.1\",\n    \"react-copy-to-clipboard\": \"^5.1.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"react-i18next\": \"^16.5.8\",\n    \"react-router\": \"^7.13.1\",\n    \"react-router-dom\": \"^7.13.1\",\n    \"reflect-metadata\": \"^0.2.2\",\n    \"tailwind-merge\": \"^3.5.0\",\n    \"yaml\": \"^2.8.2\",\n    \"zod\": \"^4.3.6\",\n    \"zustand\": \"^5.0.12\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.39.2\",\n    \"@tailwindcss/postcss\": \"^4.2.1\",\n    \"@tailwindcss/vite\": \"^4.2.1\",\n    \"@types/file-saver\": \"^2.0.7\",\n    \"@types/fs-extra\": \"^11.0.4\",\n    \"@types/node\": \"^24.10.4\",\n    \"@types/react\": \"^18.3.26\",\n    \"@types/react-copy-to-clipboard\": \"^5.0.7\",\n    \"@types/react-dom\": \"^18.3.7\",\n    \"@vitejs/plugin-legacy\": \"^7.2.1\",\n    \"@vitejs/plugin-react\": \"^5.1.4\",\n    \"eslint\": \"^9.39.2\",\n    \"eslint-config-prettier\": \"^10.1.8\",\n    \"eslint-import-resolver-typescript\": \"^4.4.4\",\n    \"eslint-plugin-better-tailwindcss\": \"^4.3.2\",\n    \"eslint-plugin-import\": \"^2.32.0\",\n    \"eslint-plugin-prettier\": \"^5.5.5\",\n    \"eslint-plugin-react-hooks\": \"^7.0.1\",\n    \"eslint-plugin-react-refresh\": \"^0.5.2\",\n    \"fs-extra\": \"^11.3.4\",\n    \"prettier\": \"^3.8.1\",\n    \"tailwindcss\": \"^4.2.1\",\n    \"typescript\": \"^5.9.3\",\n    \"typescript-eslint\": \"^8.57.0\",\n    \"vite\": \"^7.3.1\"\n  }\n}\n"
  },
  {
    "path": "ui/prettier.config.mjs",
    "content": "﻿/**\n * @type {import(\"prettier\").Config}\n */\nexport default {\n  arrowParens: \"always\",\n  bracketSpacing: true,\n  editorconfig: true,\n  htmlWhitespaceSensitivity: \"ignore\",\n  jsxSingleQuote: false,\n  endOfLine: \"crlf\",\n  printWidth: 160,\n  proseWrap: \"preserve\",\n  quoteProps: \"as-needed\",\n  semi: true,\n  singleQuote: false,\n  tabs: false,\n  tabWidth: 2,\n  trailingComma: \"es5\",\n  useTabs: false,\n};\n"
  },
  {
    "path": "ui/public/robots.txt",
    "content": "﻿User-Agent: *\nDisallow: /\n"
  },
  {
    "path": "ui/src/App.tsx",
    "content": "﻿import \"reflect-metadata\";\nimport { useEffect, useLayoutEffect, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { RouterProvider } from \"react-router-dom\";\nimport { App, ConfigProvider, type ThemeConfig, theme } from \"antd\";\nimport { type Locale } from \"antd/es/locale\";\nimport AntdLocaleEnUS from \"antd/locale/en_US\";\nimport AntdLocaleZhCN from \"antd/locale/zh_CN\";\nimport dayjs from \"dayjs\";\nimport { z } from \"zod\";\nimport { en as ZodLocaleEnUs, zhCN as ZodLocaleZhCN } from \"zod/locales\";\nimport \"dayjs/locale/zh-cn\";\n\nimport { useBrowserTheme } from \"@/hooks\";\nimport { localeNames } from \"@/i18n\";\nimport { router } from \"@/routers\";\n\nconst antdLocalesMap: Record<string, Locale> = {\n  [localeNames.EN]: AntdLocaleEnUS,\n  [localeNames.ZH]: AntdLocaleZhCN,\n};\n\nconst antdThemesMap: Record<string, ThemeConfig> = {\n  [\"light\"]: { algorithm: theme.defaultAlgorithm },\n  [\"dark\"]: { algorithm: theme.darkAlgorithm },\n};\n\nconst zodLocalesMap: Record<string, typeof ZodLocaleEnUs> = {\n  [localeNames.EN]: ZodLocaleEnUs,\n  [localeNames.ZH]: ZodLocaleZhCN,\n};\n\nconst RootApp = () => {\n  const { i18n } = useTranslation();\n\n  const { theme: browserTheme } = useBrowserTheme();\n\n  const [antdLocale, setAntdLocale] = useState(antdLocalesMap[i18n.language]);\n  const [antdTheme, setAntdTheme] = useState(antdThemesMap[browserTheme]);\n\n  const handleLanguageChanged = () => {\n    setAntdLocale(antdLocalesMap[i18n.language]);\n    dayjs.locale(i18n.language);\n    z.config(zodLocalesMap[i18n.language]?.());\n  };\n\n  i18n.on(\"initialized\", handleLanguageChanged);\n  i18n.on(\"languageChanged\", handleLanguageChanged);\n  useLayoutEffect(() => {\n    handleLanguageChanged();\n\n    return () => {\n      i18n.off(\"initialized\", handleLanguageChanged);\n      i18n.off(\"languageChanged\", handleLanguageChanged);\n    };\n  }, [i18n]);\n\n  useEffect(() => {\n    setAntdTheme(antdThemesMap[browserTheme]);\n\n    const root = window.document.documentElement;\n    root.classList.remove(\"light\", \"dark\");\n    root.classList.add(browserTheme);\n  }, [browserTheme]);\n\n  return (\n    <ConfigProvider\n      locale={antdLocale}\n      theme={{\n        ...antdTheme,\n        token: {\n          /* @see global.css, YOU MUST MODIFY BOTH DEFINITIONS AT THE SAME TIME! */\n          colorBgBase: browserTheme === \"dark\" ? \"#17191c\" : \"#ffffff\",\n          colorTextBase: browserTheme === \"dark\" ? \"#fafaf9\" : \"#141414\",\n          colorPrimary: browserTheme === \"dark\" ? \"#f97316\" : \"#ea580c\",\n          colorLink: browserTheme === \"dark\" ? \"#f97316\" : \"#ea580c\",\n          colorInfo: browserTheme === \"dark\" ? \"#478be6\" : \"#0969da\",\n          colorSuccess: browserTheme === \"dark\" ? \"#57ab5a\" : \"#1a7f37\",\n          colorWarning: browserTheme === \"dark\" ? \"#daaa3f\" : \"#eac54f\",\n          colorError: browserTheme === \"dark\" ? \"#e5534b\" : \"#d1242f\",\n\n          /* @see https://tailwindcss.com/docs/responsive-design#overview */\n          screenXS: 30 * 16,\n          screenXSMin: 30 * 16,\n          screenXSMax: 40 * 16 - 1,\n          screenSM: 40 * 16,\n          screenSMMin: 40 * 16,\n          screenSMMax: 48 * 16 - 1,\n          screenMD: 48 * 16,\n          screenMDMin: 48 * 16,\n          screenMDMax: 64 * 16 - 1,\n          screenLG: 64 * 16,\n          screenLGMin: 64 * 16,\n          screenLGMax: 80 * 16 - 1,\n          screenXL: 80 * 16,\n          screenXLMin: 80 * 16,\n          screenXLMax: 96 * 16 - 1,\n          screenXXL: 96 * 16,\n          screenXXLMin: 96 * 16,\n          padding: 16,\n          paddingXS: 8,\n          paddingXXS: 6,\n        },\n        components: {\n          Layout: {\n            ...antdTheme?.components?.Layout,\n            bodyBg: \"transparent\",\n            headerBg: \"transparent\",\n            siderBg: \"transparent\",\n          },\n          Dropdown: {\n            ...antdTheme?.components?.Dropdown,\n            paddingBlock: 9,\n          },\n          Form: {\n            ...antdTheme?.components?.Form,\n            itemMarginBottom: 28,\n          },\n        },\n      }}\n    >\n      <App>\n        <RouterProvider router={router} />\n      </App>\n    </ConfigProvider>\n  );\n};\n\nexport default RootApp;\n"
  },
  {
    "path": "ui/src/api/certificates.ts",
    "content": "import { ClientResponseError } from \"pocketbase\";\n\nimport { type CertificateFormatType } from \"@/domain/certificate\";\nimport { getPocketBase } from \"@/repository/_pocketbase\";\n\nexport const download = async (certificateId: string, format?: CertificateFormatType) => {\n  const pb = getPocketBase();\n\n  type RespData = {\n    fileBytes: string;\n  };\n  const resp = await pb.send<BaseResponse<RespData>>(`/api/certificates/${encodeURIComponent(certificateId)}/download`, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n    body: {\n      format: format,\n    },\n  });\n\n  if (resp.code != 0) {\n    throw new ClientResponseError({ status: resp.code, response: resp, data: {} });\n  }\n\n  return resp;\n};\n\nexport const revoke = async (certificateId: string) => {\n  const pb = getPocketBase();\n\n  const resp = await pb.send<BaseResponse>(`/api/certificates/${encodeURIComponent(certificateId)}/revoke`, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n  });\n\n  if (resp.code != 0) {\n    throw new ClientResponseError({ status: resp.code, response: resp, data: {} });\n  }\n\n  return resp;\n};\n"
  },
  {
    "path": "ui/src/api/notifications.ts",
    "content": "import { ClientResponseError } from \"pocketbase\";\n\nimport { getPocketBase } from \"@/repository/_pocketbase\";\n\nexport const testPushNotification = async ({ provider, accessId }: { provider: string; accessId: string }) => {\n  const pb = getPocketBase();\n\n  const resp = await pb.send<BaseResponse>(\"/api/notifications/test\", {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n    body: {\n      provider,\n      accessId,\n    },\n  });\n\n  if (resp.code != 0) {\n    throw new ClientResponseError({ status: resp.code, response: resp, data: {} });\n  }\n\n  return resp;\n};\n"
  },
  {
    "path": "ui/src/api/statistics.ts",
    "content": "import { ClientResponseError } from \"pocketbase\";\n\nimport { type Statistics } from \"@/domain/statistics\";\nimport { getPocketBase } from \"@/repository/_pocketbase\";\n\nexport const get = async () => {\n  const pb = getPocketBase();\n\n  const resp = await pb.send<BaseResponse<Statistics>>(\"/api/statistics\", {\n    method: \"GET\",\n  });\n\n  if (resp.code != 0) {\n    throw new ClientResponseError({ status: resp.code, response: resp, data: {} });\n  }\n\n  return resp;\n};\n"
  },
  {
    "path": "ui/src/api/workflows.ts",
    "content": "import { ClientResponseError } from \"pocketbase\";\n\nimport { WORKFLOW_TRIGGERS } from \"@/domain/workflow\";\nimport { getPocketBase } from \"@/repository/_pocketbase\";\n\nexport const getStats = async () => {\n  const pb = getPocketBase();\n\n  type RespData = {\n    concurrency: number;\n    pendingRunIds: string[];\n    processingRunIds: string[];\n  };\n  const resp = await pb.send<BaseResponse<RespData>>(`/api/workflows/stats`, {\n    method: \"GET\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n  });\n\n  if (resp.code != 0) {\n    throw new ClientResponseError({ status: resp.code, response: resp, data: {} });\n  }\n\n  return resp;\n};\n\nexport const startRun = async (workflowId: string) => {\n  const pb = getPocketBase();\n\n  const resp = await pb.send<BaseResponse>(`/api/workflows/${encodeURIComponent(workflowId)}/runs`, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n    body: {\n      trigger: WORKFLOW_TRIGGERS.MANUAL,\n    },\n  });\n\n  if (resp.code != 0) {\n    throw new ClientResponseError({ status: resp.code, response: resp, data: {} });\n  }\n\n  return resp;\n};\n\nexport const cancelRun = async (workflowId: string, runId: string) => {\n  const pb = getPocketBase();\n\n  const resp = await pb.send<BaseResponse>(`/api/workflows/${encodeURIComponent(workflowId)}/runs/${encodeURIComponent(runId)}/cancel`, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n  });\n\n  if (resp.code != 0) {\n    throw new ClientResponseError({ status: resp.code, response: resp, data: {} });\n  }\n\n  return resp;\n};\n"
  },
  {
    "path": "ui/src/components/AppDocument.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { IconBook } from \"@tabler/icons-react\";\nimport { Typography } from \"antd\";\n\nimport { APP_DOCUMENT_URL } from \"@/domain/app\";\n\nexport interface AppDocumentLinkButtonProps {\n  className?: string;\n  style?: React.CSSProperties;\n  showIcon?: boolean;\n}\n\nconst AppDocumentLinkButton = ({ className, style, showIcon = true }: AppDocumentLinkButtonProps) => {\n  const { t } = useTranslation();\n\n  const handleDocumentClick = () => {\n    window.open(APP_DOCUMENT_URL, \"_blank\");\n  };\n\n  return (\n    <Typography.Link className={className} style={style} type=\"secondary\" onClick={handleDocumentClick}>\n      <div className=\"flex items-center justify-center space-x-1\">\n        {showIcon ? <IconBook size=\"1em\" /> : <></>}\n        <span>{t(\"common.menu.document\")}</span>\n      </div>\n    </Typography.Link>\n  );\n};\n\nexport default {\n  LinkButton: AppDocumentLinkButton,\n};\n"
  },
  {
    "path": "ui/src/components/AppLocale.tsx",
    "content": "﻿import { useTranslation } from \"react-i18next\";\nimport { IconLanguage, type IconProps } from \"@tabler/icons-react\";\nimport { Dropdown, type DropdownProps, Typography } from \"antd\";\n\nimport { IconLanguageEnZh, IconLanguageZhEn } from \"@/components/icons\";\nimport Show from \"@/components/Show\";\nimport { localeNames, localeResources } from \"@/i18n\";\nimport { mergeCls } from \"@/utils/css\";\n\nexport const useAppLocaleMenuItems = () => {\n  const { i18n } = useTranslation();\n\n  const items = Object.keys(i18n.store.data).map((key) => {\n    return {\n      key: key as string,\n      label: i18n.store.data[key].name as string,\n      onClick: () => {\n        if (key !== (i18n.resolvedLanguage ?? i18n.language)) {\n          i18n.changeLanguage(key);\n          window.location.reload();\n        }\n      },\n    };\n  });\n\n  return items;\n};\n\nexport interface AppLocaleDropdownProps {\n  children?: React.ReactNode;\n  trigger?: DropdownProps[\"trigger\"];\n}\n\nconst AppLocaleDropdown = ({ children, trigger = [\"click\"] }: AppLocaleDropdownProps) => {\n  const items = useAppLocaleMenuItems();\n\n  return (\n    <Dropdown menu={{ items }} trigger={trigger}>\n      {children}\n    </Dropdown>\n  );\n};\n\nexport interface AppLocaleIconProps extends IconProps {}\n\nconst AppLocaleIcon = (props: AppLocaleIconProps) => {\n  const { i18n } = useTranslation();\n\n  return (\n    <Show>\n      <Show.Case when={(i18n.resolvedLanguage ?? i18n.language) === localeNames.EN}>\n        <IconLanguageEnZh {...props} />\n      </Show.Case>\n      <Show.Case when={(i18n.resolvedLanguage ?? i18n.language) === localeNames.ZH}>\n        <IconLanguageZhEn {...props} />\n      </Show.Case>\n      <Show.Default>\n        <IconLanguage {...props} />\n      </Show.Default>\n    </Show>\n  );\n};\n\nexport interface AppLocaleLinkButtonProps {\n  className?: string;\n  style?: React.CSSProperties;\n  showIcon?: boolean;\n}\n\nconst AppLocaleLinkButton = ({ className, style, showIcon = true }: AppLocaleLinkButtonProps) => {\n  const { t } = useTranslation();\n  const { i18n } = useTranslation();\n\n  return (\n    <AppLocaleDropdown trigger={[\"click\", \"hover\"]}>\n      <Typography.Text className={mergeCls(\"cursor-pointer\", className)} style={style} type=\"secondary\">\n        <div className=\"flex items-center justify-center space-x-1\">\n          {showIcon ? <AppLocaleIcon size=\"1em\" /> : <></>}\n          <span>{String(localeResources[i18n.resolvedLanguage ?? i18n.language]?.name ?? t(\"common.menu.locale\"))}</span>\n        </div>\n      </Typography.Text>\n    </AppLocaleDropdown>\n  );\n};\n\nexport default {\n  Dropdown: AppLocaleDropdown,\n  Icon: AppLocaleIcon,\n  LinkButton: AppLocaleLinkButton,\n};\n"
  },
  {
    "path": "ui/src/components/AppTheme.tsx",
    "content": "﻿import { useTranslation } from \"react-i18next\";\nimport { IconMoon, type IconProps, IconSun, IconSunMoon } from \"@tabler/icons-react\";\nimport { Dropdown, type DropdownProps, Typography } from \"antd\";\n\nimport Show from \"@/components/Show\";\nimport { useBrowserTheme } from \"@/hooks\";\nimport { mergeCls } from \"@/utils/css\";\n\nexport const useAppThemeMenuItems = () => {\n  const { t } = useTranslation();\n\n  const { themeMode, setThemeMode } = useBrowserTheme();\n\n  const items = (\n    [\n      [\"light\", \"common.theme.light\", <IconSun size=\"1em\" />],\n      [\"dark\", \"common.theme.dark\", <IconMoon size=\"1em\" />],\n      [\"system\", \"common.theme.system\", <IconSunMoon size=\"1em\" />],\n    ] satisfies Array<[string, string, React.ReactNode]>\n  ).map(([key, label, icon]) => {\n    return {\n      key: key,\n      label: t(label),\n      icon: icon,\n      onClick: () => {\n        if (key !== themeMode) {\n          setThemeMode(key as Parameters<typeof setThemeMode>[0]);\n          window.location.reload();\n        }\n      },\n    };\n  });\n\n  return items;\n};\n\nexport interface AppThemeDropdownProps {\n  children?: React.ReactNode;\n  trigger?: DropdownProps[\"trigger\"];\n}\n\nconst AppThemeDropdown = ({ children, trigger = [\"click\"] }: AppThemeDropdownProps) => {\n  const items = useAppThemeMenuItems();\n\n  return (\n    <Dropdown menu={{ items }} trigger={trigger}>\n      {children}\n    </Dropdown>\n  );\n};\n\nexport interface AppThemeIconProps extends IconProps {}\n\nconst AppThemeIcon = (props: AppThemeIconProps) => {\n  const { theme } = useBrowserTheme();\n\n  return (\n    <Show>\n      <Show.Case when={theme === \"dark\"}>\n        <IconMoon {...props} />\n      </Show.Case>\n      <Show.Default>\n        <IconSun {...props} />\n      </Show.Default>\n    </Show>\n  );\n};\n\nexport interface AppThemeLinkButtonProps {\n  className?: string;\n  style?: React.CSSProperties;\n  showIcon?: boolean;\n}\n\nconst AppThemeLinkButton = ({ className, style, showIcon = true }: AppThemeLinkButtonProps) => {\n  const { t } = useTranslation();\n\n  const { themeMode } = useBrowserTheme();\n\n  return (\n    <AppThemeDropdown trigger={[\"click\", \"hover\"]}>\n      <Typography.Text className={mergeCls(\"cursor-pointer\", className)} style={style} type=\"secondary\">\n        <div className=\"flex items-center justify-center space-x-1\">\n          {showIcon ? <AppThemeIcon size=\"1em\" /> : <></>}\n          <span>{t(`common.theme.${themeMode}`)}</span>\n        </div>\n      </Typography.Text>\n    </AppThemeDropdown>\n  );\n};\n\nexport default {\n  Dropdown: AppThemeDropdown,\n  Icon: AppThemeIcon,\n  LinkButton: AppThemeLinkButton,\n};\n"
  },
  {
    "path": "ui/src/components/AppVersion.tsx",
    "content": "import { Badge, Typography } from \"antd\";\n\nimport { APP_DOWNLOAD_URL, APP_VERSION } from \"@/domain/app\";\nimport { useVersionChecker } from \"@/hooks\";\n\nexport interface AppVersionLinkButtonProps {\n  className?: string;\n  style?: React.CSSProperties;\n}\n\nconst AppVersionLinkButton = ({ className, style }: AppVersionLinkButtonProps) => {\n  return (\n    <AppVersionBadge>\n      <Typography.Link className={className} style={style} type=\"secondary\" href={APP_DOWNLOAD_URL} target=\"_blank\">\n        {APP_VERSION}\n      </Typography.Link>\n    </AppVersionBadge>\n  );\n};\n\nexport interface AppVersionBadgeProps {\n  className?: string;\n  style?: React.CSSProperties;\n  children?: React.ReactNode;\n}\n\nconst AppVersionBadge = ({ className, style, children }: AppVersionBadgeProps) => {\n  const { hasUpdate } = useVersionChecker();\n\n  return (\n    <Badge\n      className={className}\n      style={style}\n      styles={{\n        indicator: { transform: \"scale(0.75) translate(50%, -85%)\" },\n      }}\n      count={hasUpdate ? \"NEW\" : void 0}\n    >\n      {children}\n    </Badge>\n  );\n};\n\nexport default {\n  LinkButton: AppVersionLinkButton,\n  Badge: AppVersionBadge,\n};\n"
  },
  {
    "path": "ui/src/components/CodeTextInput.tsx",
    "content": "﻿import { useContext, useMemo, useRef } from \"react\";\nimport { json } from \"@codemirror/lang-json\";\nimport { yaml } from \"@codemirror/lang-yaml\";\nimport { StreamLanguage } from \"@codemirror/language\";\nimport { powerShell } from \"@codemirror/legacy-modes/mode/powershell\";\nimport { shell } from \"@codemirror/legacy-modes/mode/shell\";\nimport { basicSetup } from \"@uiw/codemirror-extensions-basic-setup\";\nimport { vscodeDark, vscodeLight } from \"@uiw/codemirror-theme-vscode\";\nimport CodeMirror, { EditorView, type ReactCodeMirrorProps, type ReactCodeMirrorRef } from \"@uiw/react-codemirror\";\nimport { useFocusWithin, useHover } from \"ahooks\";\nimport { theme } from \"antd\";\nimport DisabledContext from \"antd/es/config-provider/DisabledContext\";\n\nimport { useBrowserTheme } from \"@/hooks\";\nimport { mergeCls } from \"@/utils/css\";\n\nexport interface CodeTextInputProps extends Omit<ReactCodeMirrorProps, \"extensions\" | \"lang\" | \"theme\"> {\n  disabled?: boolean;\n  language?: string | string[];\n  lineNumbers?: boolean;\n  lineWrapping?: boolean;\n  readOnly?: boolean;\n}\n\nconst CodeTextInput = ({ className, style, disabled, language, lineNumbers = true, lineWrapping = true, readOnly, ...props }: CodeTextInputProps) => {\n  const { token: themeToken } = theme.useToken();\n\n  const { theme: browserTheme } = useBrowserTheme();\n\n  const injectedDisabled = useContext(DisabledContext);\n  const mergedDisabled = disabled ?? injectedDisabled;\n\n  const cmRef = useRef<ReactCodeMirrorRef>(null);\n  const isFocusing = useFocusWithin(cmRef.current?.editor);\n  const isHovering = useHover(cmRef.current?.editor);\n\n  const cmTheme = useMemo(() => {\n    if (browserTheme === \"dark\") {\n      return vscodeDark;\n    }\n    return vscodeLight;\n  }, [browserTheme]);\n\n  const cmExtensions = useMemo(() => {\n    const temp: NonNullable<ReactCodeMirrorProps[\"extensions\"]> = [\n      basicSetup({\n        foldGutter: false,\n        dropCursor: false,\n        allowMultipleSelections: false,\n        indentOnInput: false,\n      }),\n    ];\n\n    if (lineWrapping) {\n      temp.push(EditorView.lineWrapping);\n    }\n\n    const langs = Array.isArray(language) ? language : [language];\n    langs.forEach((lang) => {\n      switch (lang) {\n        case \"shell\":\n          temp.push(StreamLanguage.define(shell));\n          break;\n        case \"json\":\n          temp.push(json());\n          break;\n        case \"powershell\":\n          temp.push(StreamLanguage.define(powerShell));\n          break;\n        case \"yaml\":\n          temp.push(yaml());\n          break;\n      }\n    });\n\n    return temp;\n  }, [language, lineWrapping]);\n\n  return (\n    <div\n      className={mergeCls(\"ant-input\", className)}\n      style={{\n        ...style,\n        paddingBlock: themeToken.Input?.paddingBlock,\n        paddingInline: themeToken.Input?.paddingInline,\n        fontSize: themeToken.Input?.inputFontSize,\n        lineHeight: themeToken.lineHeight,\n        color: mergedDisabled ? themeToken.colorTextDisabled : themeToken.colorText,\n        backgroundColor: mergedDisabled\n          ? themeToken.colorBgContainerDisabled\n          : isFocusing\n            ? (themeToken.Input?.activeBg ?? themeToken.colorBgContainer)\n            : isHovering\n              ? (themeToken.Input?.hoverBg ?? themeToken.colorBgContainer)\n              : void 0,\n        borderWidth: `${themeToken.lineWidth}px`,\n        borderStyle: themeToken.lineType,\n        borderColor: isFocusing\n          ? (themeToken.Input?.activeBorderColor ?? themeToken.colorPrimaryActive)\n          : isHovering\n            ? (themeToken.Input?.hoverBorderColor ?? themeToken.colorPrimaryHover)\n            : themeToken.colorBorder,\n        borderRadius: `${themeToken.borderRadius}px`,\n        boxShadow: isFocusing ? themeToken.Input?.activeShadow : void 0,\n        overflow: \"hidden\",\n      }}\n    >\n      <CodeMirror\n        ref={cmRef}\n        height=\"100%\"\n        style={{ height: \"100%\" }}\n        {...props}\n        basicSetup={{\n          allowMultipleSelections: false,\n          dropCursor: false,\n          foldGutter: false,\n          lineNumbers: lineNumbers,\n          indentOnInput: false,\n        }}\n        extensions={cmExtensions}\n        readOnly={readOnly || mergedDisabled}\n        theme={cmTheme}\n      />\n    </div>\n  );\n};\n\nexport default CodeTextInput;\n"
  },
  {
    "path": "ui/src/components/CopyableText.tsx",
    "content": "﻿import { CopyToClipboard } from \"react-copy-to-clipboard\";\nimport { useTranslation } from \"react-i18next\";\nimport { App, Button } from \"antd\";\n\nexport interface CopyableTextProps {\n  className?: string;\n  style?: React.CSSProperties;\n  children?: React.ReactNode;\n  text?: string;\n}\n\nconst CopyableText = ({ className, style, children, text }: CopyableTextProps) => {\n  const { t } = useTranslation();\n\n  const { message } = App.useApp();\n\n  return (\n    <CopyToClipboard\n      text={text ?? (children as string)}\n      onCopy={() => {\n        message.success(t(\"common.text.copied\"));\n      }}\n    >\n      <Button className={className} style={style} size=\"small\" type=\"text\">\n        {children}\n      </Button>\n    </CopyToClipboard>\n  );\n};\n\nexport default CopyableText;\n"
  },
  {
    "path": "ui/src/components/DrawerForm.tsx",
    "content": "﻿import { useTranslation } from \"react-i18next\";\nimport { IconX } from \"@tabler/icons-react\";\nimport { useControllableValue } from \"ahooks\";\nimport { Button, Drawer, type DrawerProps, Flex, Form, type FormProps, type ModalProps } from \"antd\";\n\nimport { useAntdForm, useTriggerElement } from \"@/hooks\";\n\nexport interface DrawerFormProps<T extends NonNullable<unknown> = any> extends Omit<FormProps<T>, \"title\" | \"onFinish\"> {\n  className?: string;\n  style?: React.CSSProperties;\n  children?: React.ReactNode;\n  cancelButtonProps?: ModalProps[\"cancelButtonProps\"];\n  cancelText?: ModalProps[\"cancelText\"];\n  defaultOpen?: boolean;\n  drawerProps?: Omit<DrawerProps, \"defaultOpen\" | \"forceRender\" | \"open\" | \"title\" | \"width\" | \"onOpenChange\">;\n  okButtonProps?: ModalProps[\"okButtonProps\"];\n  okText?: ModalProps[\"okText\"];\n  open?: boolean;\n  title?: React.ReactNode;\n  trigger?: React.ReactNode;\n  onFinish?: (values: T) => unknown | Promise<unknown>;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst DrawerForm = <T extends NonNullable<unknown> = any>({\n  className,\n  style,\n  children,\n  cancelText,\n  cancelButtonProps,\n  form,\n  drawerProps,\n  okText,\n  okButtonProps,\n  title,\n  trigger,\n  onFinish,\n  ...props\n}: DrawerFormProps<T>) => {\n  const { t } = useTranslation();\n\n  const [open, setOpen] = useControllableValue<boolean>(props, {\n    valuePropName: \"open\",\n    defaultValuePropName: \"defaultOpen\",\n    trigger: \"onOpenChange\",\n  });\n\n  const triggerEl = useTriggerElement(trigger, {\n    onClick: () => {\n      setOpen(true);\n    },\n  });\n\n  const {\n    form: formInst,\n    formPending,\n    formProps,\n    submit: submitForm,\n  } = useAntdForm({\n    form,\n    onSubmit: (values) => {\n      return onFinish?.(values);\n    },\n  });\n\n  const mergedFormProps: FormProps = {\n    clearOnDestroy: drawerProps?.destroyOnHidden ? true : void 0,\n    ...formProps,\n    ...props,\n  };\n\n  const mergedDrawerProps: DrawerProps = {\n    ...drawerProps,\n    closeIcon: false,\n    onClose: async (e) => {\n      if (formPending) return;\n\n      // 关闭 Drawer 时 Promise.reject 阻止关闭\n      await drawerProps?.onClose?.(e);\n      setOpen(false);\n\n      if (!mergedFormProps.preserve) {\n        formInst.resetFields();\n      }\n    },\n  };\n\n  const handleOkClick = async () => {\n    // 提交表单返回 Promise.reject 时不关闭 Drawer\n    await submitForm();\n\n    setOpen(false);\n  };\n\n  const handleCancelClick = () => {\n    if (formPending) return;\n\n    setOpen(false);\n  };\n\n  return (\n    <>\n      {triggerEl}\n\n      <Drawer\n        {...mergedDrawerProps}\n        footer={\n          <Flex className=\"px-2\" justify=\"end\" gap=\"small\">\n            <Button {...cancelButtonProps} onClick={handleCancelClick}>\n              {cancelText ?? t(\"common.button.cancel\")}\n            </Button>\n            <Button {...okButtonProps} type=\"primary\" loading={formPending} onClick={handleOkClick}>\n              {okText ?? t(\"common.button.ok\")}\n            </Button>\n          </Flex>\n        }\n        forceRender\n        open={open}\n        title={\n          <Flex align=\"center\" justify=\"space-between\" gap=\"small\">\n            <div className=\"flex-1 truncate\">{title}</div>\n            {mergedDrawerProps.closeIcon !== false && (\n              <Button\n                className=\"ant-drawer-close\"\n                style={{ marginInline: 0 }}\n                icon={mergedDrawerProps.closeIcon ?? <IconX size=\"1.25em\" />}\n                size=\"small\"\n                type=\"text\"\n                onClick={handleCancelClick}\n              />\n            )}\n          </Flex>\n        }\n      >\n        <Form className={className} style={style} {...mergedFormProps} form={formInst}>\n          {children}\n        </Form>\n      </Drawer>\n    </>\n  );\n};\n\nexport default DrawerForm;\n"
  },
  {
    "path": "ui/src/components/Empty.tsx",
    "content": "﻿import { useTranslation } from \"react-i18next\";\nimport { Typography, theme } from \"antd\";\n\nimport Show from \"./Show\";\n\nexport interface EmptyProps {\n  className?: string;\n  style?: React.CSSProperties;\n  title?: React.ReactNode;\n  description?: React.ReactNode;\n  extra?: React.ReactNode;\n  icon?: React.ReactNode;\n}\n\nconst Empty = (props: EmptyProps) => {\n  const { t } = useTranslation();\n\n  const { className, style, title = t(\"common.text.nodata\"), description, extra, icon } = props;\n\n  const { token: themeToken } = theme.useToken();\n\n  const isPrimitive = (node: React.ReactNode): node is string | number | boolean | null => {\n    return typeof node === \"string\" || typeof node === \"number\" || typeof node === \"boolean\" || node == null;\n  };\n\n  return (\n    <div className={className} style={style}>\n      <div className=\"relative w-full overflow-hidden\">\n        <div className=\"relative top-0 left-0 z-1 flex h-full w-full py-4\">\n          <div className=\"relative mx-auto w-full max-w-lg\">\n            <div className=\"flex flex-col gap-2 text-center\">\n              <Show when={!!icon}>\n                <div className=\"mx-auto\">\n                  <div className=\"flex size-12 items-center justify-center overflow-hidden rounded-md border text-gray-600 shadow-sm dark:text-gray-200\">\n                    {icon}\n                  </div>\n                </div>\n              </Show>\n              <div className=\"my-2\">\n                <Show when={!!title}>\n                  <div className=\"pb-2 text-xl\" style={{ color: themeToken.colorTextLabel }}>\n                    {title}\n                  </div>\n                </Show>\n                <Show when={!!description}>\n                  {isPrimitive(description) ? (\n                    <Typography.Text type=\"secondary\">{description}</Typography.Text>\n                  ) : (\n                    <div style={{ color: themeToken.colorTextSecondary }}>{description}</div>\n                  )}\n                </Show>\n              </div>\n              <Show when={!!extra}>\n                <div>{extra}</div>\n              </Show>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default Empty;\n"
  },
  {
    "path": "ui/src/components/FileTextInput.tsx",
    "content": "﻿import { type ChangeEvent, useContext, useRef } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { IconFileImport } from \"@tabler/icons-react\";\nimport { Button, type ButtonProps, Input } from \"antd\";\nimport DisabledContext from \"antd/es/config-provider/DisabledContext\";\nimport { type TextAreaProps } from \"antd/es/input/TextArea\";\n\nimport { readFileAsText } from \"@/utils/file\";\n\nexport interface FileTextInputProps extends Omit<TextAreaProps, \"onChange\"> {\n  accept?: string;\n  uploadButtonProps?: Omit<ButtonProps, \"disabled\" | \"onClick\">;\n  uploadText?: string;\n  onChange?: (value: string) => void;\n}\n\nconst FileTextInput = ({ className, style, accept, disabled, readOnly, uploadText, uploadButtonProps, onChange, ...props }: FileTextInputProps) => {\n  const { t } = useTranslation();\n\n  const injectedDisabled = useContext(DisabledContext);\n  const mergedDisabled = disabled ?? injectedDisabled;\n\n  const fileInputRef = useRef<HTMLInputElement>(null);\n\n  const handleButtonClick = () => {\n    if (fileInputRef.current) {\n      fileInputRef.current.click();\n    }\n  };\n\n  const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {\n    const { files } = e.target as HTMLInputElement;\n    if (files?.length) {\n      const value = await readFileAsText(files[0]);\n      onChange?.(value);\n    }\n  };\n\n  return (\n    <div className={className} style={style}>\n      <div className=\"flex flex-col items-center gap-2\">\n        <Input.TextArea {...props} disabled={mergedDisabled} readOnly={readOnly} onChange={(e) => onChange?.(e.target.value)} />\n        {!readOnly && (\n          <>\n            <Button {...uploadButtonProps} block disabled={mergedDisabled} icon={<IconFileImport size=\"1.25em\" />} onClick={handleButtonClick}>\n              {uploadText ?? t(\"common.text.import_from_file\")}\n            </Button>\n            <input ref={fileInputRef} type=\"file\" style={{ display: \"none\" }} accept={accept} onChange={handleFileChange} />\n          </>\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport default FileTextInput;\n"
  },
  {
    "path": "ui/src/components/ModalForm.tsx",
    "content": "import { useControllableValue } from \"ahooks\";\nimport { Form, type FormProps, Modal, type ModalProps } from \"antd\";\n\nimport { useAntdForm, useTriggerElement } from \"@/hooks\";\n\nexport interface ModalFormProps<T extends NonNullable<unknown> = any> extends Omit<FormProps<T>, \"title\" | \"onFinish\"> {\n  className?: string;\n  style?: React.CSSProperties;\n  children?: React.ReactNode;\n  cancelButtonProps?: ModalProps[\"cancelButtonProps\"];\n  cancelText?: ModalProps[\"cancelText\"];\n  defaultOpen?: boolean;\n  modalProps?: Omit<\n    ModalProps,\n    | \"cancelButtonProps\"\n    | \"cancelText\"\n    | \"confirmLoading\"\n    | \"defaultOpen\"\n    | \"forceRender\"\n    | \"okButtonProps\"\n    | \"okText\"\n    | \"okType\"\n    | \"open\"\n    | \"title\"\n    | \"width\"\n    | \"onCancel\"\n    | \"onOk\"\n    | \"onOpenChange\"\n  >;\n  okButtonProps?: ModalProps[\"okButtonProps\"];\n  okText?: ModalProps[\"okText\"];\n  open?: boolean;\n  title?: ModalProps[\"title\"];\n  trigger?: React.ReactNode;\n  width?: ModalProps[\"width\"];\n  onFinish?: (values: T) => unknown | Promise<unknown>;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst ModalForm = <T extends NonNullable<unknown> = any>({\n  className,\n  style,\n  children,\n  cancelButtonProps,\n  cancelText,\n  form,\n  modalProps,\n  okButtonProps,\n  okText,\n  title,\n  trigger,\n  width,\n  onFinish,\n  ...props\n}: ModalFormProps<T>) => {\n  const [open, setOpen] = useControllableValue<boolean>(props, {\n    valuePropName: \"open\",\n    defaultValuePropName: \"defaultOpen\",\n    trigger: \"onOpenChange\",\n  });\n\n  const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) });\n\n  const {\n    form: formInst,\n    formPending,\n    formProps,\n    submit: submitForm,\n  } = useAntdForm({\n    form,\n    onSubmit: (values) => {\n      return onFinish?.(values);\n    },\n  });\n\n  const mergedFormProps: FormProps = {\n    clearOnDestroy: modalProps?.destroyOnHidden ? true : void 0,\n    ...formProps,\n    ...props,\n  };\n\n  const mergedModalProps: ModalProps = {\n    ...modalProps,\n    afterClose: () => {\n      if (!mergedFormProps.preserve) {\n        formInst.resetFields();\n      }\n\n      modalProps?.afterClose?.();\n    },\n  };\n\n  const handleOkClick = async () => {\n    // 提交表单返回 Promise.reject 时不关闭 Modal\n    await submitForm();\n    setOpen(false);\n  };\n\n  const handleCancelClick = () => {\n    if (formPending) return;\n\n    setOpen(false);\n  };\n\n  return (\n    <>\n      {triggerEl}\n\n      <Modal\n        {...mergedModalProps}\n        cancelButtonProps={cancelButtonProps}\n        cancelText={cancelText}\n        confirmLoading={formPending}\n        forceRender\n        okButtonProps={okButtonProps}\n        okText={okText}\n        okType=\"primary\"\n        open={open}\n        title={title}\n        width={width}\n        onOk={handleOkClick}\n        onCancel={handleCancelClick}\n      >\n        <div className=\"py-3\">\n          <Form className={className} style={style} {...mergedFormProps} form={formInst}>\n            {children}\n          </Form>\n        </div>\n      </Modal>\n    </>\n  );\n};\n\nexport default ModalForm;\n"
  },
  {
    "path": "ui/src/components/MultipleInput.tsx",
    "content": "﻿import { type ChangeEvent, forwardRef, useImperativeHandle, useMemo, useRef } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { IconCircleArrowDown, IconCircleArrowUp, IconCircleMinus, IconCirclePlus } from \"@tabler/icons-react\";\nimport { useControllableValue } from \"ahooks\";\nimport { Button, Input, type InputProps, type InputRef } from \"antd\";\nimport { produce } from \"immer\";\n\nexport interface MultipleInputProps extends Omit<InputProps, \"count\" | \"defaultValue\" | \"showCount\" | \"value\" | \"onChange\" | \"onPressEnter\" | \"onClear\"> {\n  allowClear?: boolean;\n  defaultValue?: string[];\n  maxCount?: number;\n  minCount?: number;\n  showSortButton?: boolean;\n  value?: string[];\n  onChange?: (value: string[]) => void;\n  onValueChange?: (index: number, element: string) => void;\n  onValueCreate?: (index: number) => void;\n  onValueRemove?: (index: number) => void;\n  onValueSort?: (oldIndex: number, newIndex: number) => void;\n}\n\nconst MultipleInput = ({\n  allowClear = false,\n  disabled,\n  maxCount,\n  minCount,\n  showSortButton = true,\n  onValueChange,\n  onValueCreate,\n  onValueSort,\n  onValueRemove,\n  ...props\n}: MultipleInputProps) => {\n  const { t } = useTranslation();\n\n  const itemRefs = useRef<MultipleInputItemInstance[]>([]);\n\n  const [value, setValue] = useControllableValue<string[]>(props, {\n    valuePropName: \"value\",\n    defaultValue: [],\n    defaultValuePropName: \"defaultValue\",\n    trigger: \"onChange\",\n  });\n\n  const handleCreate = () => {\n    const newValue = produce(value ?? [], (draft) => {\n      draft.push(\"\");\n    });\n    setValue(newValue);\n    setTimeout(() => itemRefs.current[newValue.length - 1]?.focus(), 1);\n\n    onValueCreate?.(newValue.length - 1);\n  };\n\n  const handleChange = (index: number, element: string) => {\n    const newValue = produce(value, (draft) => {\n      draft[index] = element;\n    });\n    setValue(newValue);\n\n    onValueChange?.(index, element);\n  };\n\n  const handleInputBlur = (index: number) => {\n    if (!allowClear && !value[index]) {\n      const newValue = produce(value, (draft) => {\n        draft.splice(index, 1);\n      });\n      setValue(newValue);\n    }\n  };\n\n  const handleClickUp = (index: number) => {\n    if (index === 0) {\n      return;\n    }\n\n    const newValue = produce(value, (draft) => {\n      const temp = draft[index - 1];\n      draft[index - 1] = draft[index];\n      draft[index] = temp;\n    });\n    setValue(newValue);\n\n    onValueSort?.(index, index - 1);\n  };\n\n  const handleClickDown = (index: number) => {\n    if (index === value.length - 1) {\n      return;\n    }\n\n    const newValue = produce(value, (draft) => {\n      const temp = draft[index + 1];\n      draft[index + 1] = draft[index];\n      draft[index] = temp;\n    });\n    setValue(newValue);\n\n    onValueSort?.(index, index + 1);\n  };\n\n  const handleClickAdd = (index: number) => {\n    const newValue = produce(value, (draft) => {\n      draft.splice(index + 1, 0, \"\");\n    });\n    setValue(newValue);\n    setTimeout(() => itemRefs.current[index + 1]?.focus(), 1);\n\n    onValueCreate?.(index + 1);\n  };\n\n  const handleClickRemove = (index: number) => {\n    const newValue = produce(value, (draft) => {\n      draft.splice(index, 1);\n    });\n    setValue(newValue);\n\n    onValueRemove?.(index);\n  };\n\n  return value == null || value.length === 0 ? (\n    <Button block color=\"primary\" disabled={disabled || maxCount === 0} size={props.size} variant=\"dashed\" onClick={handleCreate}>\n      {t(\"common.button.add\")}\n    </Button>\n  ) : (\n    <div className=\"flex flex-col gap-2\">\n      {Array.from(value).map((element, index) => {\n        const allowUp = index > 0;\n        const allowDown = index < value.length - 1;\n        const allowRemove = minCount == null || value.length > minCount;\n        const allowAdd = maxCount == null || value.length < maxCount;\n\n        return (\n          <MultipleInputItem\n            {...props}\n            key={index}\n            ref={(ref) => (itemRefs.current[index] = ref!)}\n            allowAdd={allowAdd}\n            allowClear={allowClear}\n            allowDown={allowDown}\n            allowRemove={allowRemove}\n            allowUp={allowUp}\n            disabled={disabled}\n            defaultValue={void 0}\n            showSortButton={showSortButton}\n            value={element}\n            onBlur={() => handleInputBlur(index)}\n            onChange={(val) => handleChange(index, val)}\n            onEntryAdd={() => handleClickAdd(index)}\n            onEntryDown={() => handleClickDown(index)}\n            onEntryUp={() => handleClickUp(index)}\n            onEntryRemove={() => handleClickRemove(index)}\n          />\n        );\n      })}\n    </div>\n  );\n};\n\ntype MultipleInputItemProps = Omit<\n  MultipleInputProps,\n  \"defaultValue\" | \"maxCount\" | \"minCount\" | \"preset\" | \"value\" | \"onChange\" | \"onValueCreate\" | \"onValueRemove\" | \"onValueSort\" | \"onValueChange\"\n> & {\n  allowAdd: boolean;\n  allowRemove: boolean;\n  allowUp: boolean;\n  allowDown: boolean;\n  defaultValue?: string;\n  value?: string;\n  onChange?: (value: string) => void;\n  onEntryAdd?: () => void;\n  onEntryDown?: () => void;\n  onEntryUp?: () => void;\n  onEntryRemove?: () => void;\n};\n\ntype MultipleInputItemInstance = {\n  focus: InputRef[\"focus\"];\n  blur: InputRef[\"blur\"];\n  select: InputRef[\"select\"];\n};\n\nconst MultipleInputItem = forwardRef<MultipleInputItemInstance, MultipleInputItemProps>(\n  (\n    {\n      allowAdd,\n      allowClear,\n      allowDown,\n      allowRemove,\n      allowUp,\n      disabled,\n      showSortButton,\n      onEntryAdd,\n      onEntryDown,\n      onEntryUp,\n      onEntryRemove,\n      ...props\n    }: MultipleInputItemProps,\n    ref\n  ) => {\n    const inputRef = useRef<InputRef>(null);\n\n    const [value, setValue] = useControllableValue<string>(props, {\n      valuePropName: \"value\",\n      defaultValue: \"\",\n      defaultValuePropName: \"defaultValue\",\n      trigger: \"onChange\",\n    });\n\n    const upBtn = useMemo(() => {\n      if (!showSortButton) return null;\n      return <Button icon={<IconCircleArrowUp size=\"1.25em\" />} color=\"default\" disabled={disabled || !allowUp} type=\"text\" onClick={onEntryUp} />;\n    }, [allowUp, disabled, showSortButton, onEntryUp]);\n    const downBtn = useMemo(() => {\n      if (!showSortButton) return null;\n      return <Button icon={<IconCircleArrowDown size=\"1.25em\" />} color=\"default\" disabled={disabled || !allowDown} type=\"text\" onClick={onEntryDown} />;\n    }, [allowDown, disabled, showSortButton, onEntryDown]);\n    const removeBtn = useMemo(() => {\n      return <Button icon={<IconCircleMinus size=\"1.25em\" />} color=\"default\" disabled={disabled || !allowRemove} type=\"text\" onClick={onEntryRemove} />;\n    }, [allowRemove, disabled, onEntryRemove]);\n    const addBtn = useMemo(() => {\n      return <Button icon={<IconCirclePlus size=\"1.25em\" />} color=\"default\" disabled={disabled || !allowAdd} type=\"text\" onClick={onEntryAdd} />;\n    }, [allowAdd, disabled, onEntryAdd]);\n\n    const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {\n      setValue(e.target.value);\n    };\n\n    useImperativeHandle(ref, () => ({\n      focus: (options) => {\n        inputRef.current?.focus(options);\n      },\n      blur: () => {\n        inputRef.current?.blur();\n      },\n      select: () => {\n        inputRef.current?.select();\n      },\n    }));\n\n    return (\n      <div className=\"flex flex-nowrap items-center space-x-2\">\n        <div className=\"grow\">\n          <Input\n            {...props}\n            ref={inputRef}\n            className={void 0}\n            style={void 0}\n            allowClear={allowClear}\n            defaultValue={void 0}\n            value={value}\n            onChange={handleInputChange}\n          />\n        </div>\n        <div className=\"flex items-center justify-end\">\n          {removeBtn}\n          {upBtn}\n          {downBtn}\n          {addBtn}\n        </div>\n      </div>\n    );\n  }\n);\n\nexport default MultipleInput;\n"
  },
  {
    "path": "ui/src/components/MultipleSplitValueInput.tsx",
    "content": "﻿import { type ChangeEvent, useEffect } from \"react\";\nimport { IconList } from \"@tabler/icons-react\";\nimport { useControllableValue } from \"ahooks\";\nimport { Button, Form, Input, type InputProps, Space } from \"antd\";\nimport { nanoid } from \"nanoid/non-secure\";\n\nimport { useAntdForm } from \"@/hooks\";\nimport ModalForm from \"./ModalForm\";\nimport MultipleInput from \"./MultipleInput\";\n\ntype SplitOptions = {\n  removeEmpty?: boolean;\n  trimSpace?: boolean;\n};\n\nexport interface MultipleSplitValueInputProps extends Omit<InputProps, \"count\" | \"defaultValue\" | \"showCount\" | \"value\" | \"onChange\"> {\n  defaultValue?: string;\n  maxCount?: number;\n  minCount?: number;\n  modalTitle?: React.ReactNode;\n  modalWidth?: number | string;\n  placeholderInModal?: string;\n  showSortButton?: boolean;\n  separator?: string;\n  splitOptions?: SplitOptions;\n  value?: string[];\n  onChange?: (value: string) => void;\n}\n\nconst DEFAULT_SEPARATOR = \";\";\n\nconst MultipleSplitValueInput = ({\n  className,\n  style,\n  size,\n  separator: delimiter = DEFAULT_SEPARATOR,\n  disabled,\n  maxCount,\n  minCount,\n  modalTitle,\n  modalWidth = \"480px\",\n  placeholder,\n  placeholderInModal,\n  showSortButton = true,\n  splitOptions = {},\n  onClear,\n  ...props\n}: MultipleSplitValueInputProps) => {\n  const [value, setValue] = useControllableValue<string>(props, {\n    valuePropName: \"value\",\n    defaultValuePropName: \"defaultValue\",\n    trigger: \"onChange\",\n  });\n\n  const { form: formInst, formProps } = useAntdForm({\n    name: \"componentMultipleSplitValueInput_\" + nanoid(),\n    initialValues: { value: value?.split(delimiter) },\n    onSubmit: (values) => {\n      const temp = (values.value ?? []) as string[];\n      if (splitOptions.trimSpace) {\n        temp.map((e) => e.trim());\n      }\n      if (splitOptions.removeEmpty) {\n        temp.filter((e) => !!e);\n      }\n\n      setValue(temp.join(delimiter));\n    },\n  });\n\n  useEffect(() => {\n    formInst.setFieldValue(\"value\", value?.split(delimiter));\n  }, [delimiter, value]);\n\n  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n    setValue(e.target.value);\n  };\n\n  const handleClear = () => {\n    setValue(\"\");\n    onClear?.();\n  };\n\n  return (\n    <div className={className} style={style}>\n      <Space.Compact className=\"w-full\">\n        <Input {...props} disabled={disabled} placeholder={placeholder} size={size} value={value} onChange={handleChange} onClear={handleClear} />\n        <ModalForm\n          {...formProps}\n          layout=\"vertical\"\n          form={formInst}\n          modalProps={{ destroyOnHidden: true }}\n          title={modalTitle}\n          trigger={\n            <Button className=\"px-2\" disabled={disabled} size={size}>\n              <IconList size=\"1.25em\" />\n            </Button>\n          }\n          validateTrigger=\"onSubmit\"\n          width={modalWidth}\n        >\n          <Form.Item name=\"value\" noStyle>\n            <MultipleInput minCount={minCount} maxCount={maxCount} placeholder={placeholderInModal ?? placeholder} showSortButton={showSortButton} />\n          </Form.Item>\n        </ModalForm>\n      </Space.Compact>\n    </div>\n  );\n};\n\nexport default MultipleSplitValueInput;\n"
  },
  {
    "path": "ui/src/components/Show.tsx",
    "content": "import { Children as ReactChildren, isValidElement } from \"react\";\n\nexport type ShowProps =\n  | {\n      children: React.ReactNode;\n    }\n  | {\n      when: boolean;\n      children: React.ReactNode;\n      fallback?: React.ReactNode;\n    };\n\nconst Show = ({ children, ...props }: ShowProps) => {\n  if (\"when\" in props) {\n    const { when, fallback } = props;\n    return when ? children : fallback;\n  }\n\n  let fallback: React.ReactNode | undefined;\n\n  const cases = ReactChildren.toArray(children);\n  for (let i = 0; i < cases.length; i++) {\n    const child = cases[i];\n    if (isValidElement(child)) {\n      if (child.type === Case && child.props.when) {\n        return child.props.children;\n      } else if (child.type === Default) {\n        if (fallback) {\n          console.warn(\"[certimate] multiple Default components found in Show. Only the first will be used.\");\n          continue;\n        }\n        fallback = child.props.children;\n      }\n    }\n  }\n\n  return fallback;\n};\n\nconst Case = ({ children, when }: { children: React.ReactNode; when: boolean }) => {\n  return when ? children : null;\n};\n\nconst Default = ({ children }: { children: React.ReactNode }) => {\n  return children;\n};\n\nconst _default = Object.assign(Show, {\n  Case,\n  Default,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/Tips.tsx",
    "content": "﻿import { IconBulb } from \"@tabler/icons-react\";\nimport { Alert, Flex, Typography, theme } from \"antd\";\n\nexport interface TipsProps {\n  className?: string;\n  style?: React.CSSProperties;\n  message: React.ReactNode;\n}\n\nconst Tips = ({ className, style, message }: TipsProps) => {\n  const { token: themeToken } = theme.useToken();\n\n  return (\n    <Alert\n      className={className}\n      style={style}\n      title={\n        <Flex gap=\"small\">\n          <div style={{ marginTop: \"1px\" }}>\n            <IconBulb size={18} color={themeToken.colorInfo} />\n          </div>\n          <div style={{ flex: 1 }}>\n            <Typography.Text>{message}</Typography.Text>\n          </div>\n        </Flex>\n      }\n      type=\"info\"\n    />\n  );\n};\n\nexport default Tips;\n"
  },
  {
    "path": "ui/src/components/access/AccessEditDrawer.tsx",
    "content": "import { startTransition, useCallback, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { IconChevronDown, IconX } from \"@tabler/icons-react\";\nimport { useControllableValue, useGetState } from \"ahooks\";\nimport { App, Button, Drawer, Dropdown, Flex, Form, Space } from \"antd\";\n\nimport { testPushNotification } from \"@/api/notifications\";\nimport AccessProviderPicker from \"@/components/provider/AccessProviderPicker\";\nimport Show from \"@/components/Show\";\nimport { type AccessModel } from \"@/domain/access\";\nimport { ACCESS_USAGES } from \"@/domain/provider\";\nimport { useTriggerElement, useZustandShallowSelector } from \"@/hooks\";\nimport { useAccessesStore } from \"@/stores/access\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nimport AccessForm, { type AccessFormModes, type AccessFormProps, type AccessFormUsages } from \"./AccessForm\";\n\nexport interface AccessEditDrawerProps {\n  afterClose?: () => void;\n  afterSubmit?: (record: AccessModel) => void;\n  data?: AccessFormProps[\"initialValues\"];\n  loading?: boolean;\n  mode: AccessFormModes;\n  open?: boolean;\n  trigger?: React.ReactNode;\n  usage?: AccessFormUsages;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst AccessEditDrawer = ({ afterSubmit, mode, data, loading, trigger, usage, ...props }: AccessEditDrawerProps) => {\n  const { t } = useTranslation();\n\n  const { message, notification } = App.useApp();\n\n  const { createAccess, updateAccess } = useAccessesStore(useZustandShallowSelector([\"createAccess\", \"updateAccess\"]));\n\n  const [open, setOpen] = useControllableValue<boolean>(props, {\n    valuePropName: \"open\",\n    defaultValuePropName: \"defaultOpen\",\n    trigger: \"onOpenChange\",\n  });\n\n  const afterClose = () => {\n    setFormPending(false);\n    setFormChanged(false);\n    setIsTesting(false);\n    props.afterClose?.();\n  };\n\n  const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) });\n\n  const providerFilter = AccessForm.useProviderFilterByUsage(usage);\n\n  const [formInst] = Form.useForm();\n  const [formPending, setFormPending] = useState(false);\n  const [formChanged, setFormChanged] = useState(false);\n\n  const submitForm = async () => {\n    let formValues: AccessModel;\n\n    setFormPending(true);\n    try {\n      formValues = await formInst.validateFields();\n      formValues.reserve = usage === \"ca\" ? \"ca\" : usage === \"notification\" ? \"notif\" : void 0;\n    } catch (err) {\n      message.warning(t(\"common.errmsg.form_invalid\"));\n\n      setFormPending(false);\n      throw err;\n    }\n\n    try {\n      switch (mode) {\n        case \"create\":\n          {\n            if (data?.id) {\n              throw \"Invalid props: `data`\";\n            }\n\n            formValues = await createAccess(formValues);\n          }\n          break;\n\n        case \"modify\":\n          {\n            if (!data?.id) {\n              throw \"Invalid props: `data`\";\n            }\n\n            formValues = await updateAccess({ ...data, ...formValues });\n          }\n          break;\n\n        default:\n          throw \"Invalid props: `mode`\";\n      }\n\n      afterSubmit?.(formValues);\n    } catch (err) {\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n\n      throw err;\n    } finally {\n      setFormPending(false);\n    }\n  };\n\n  const fieldProvider = Form.useWatch<string>(\"provider\", { form: formInst, preserve: true });\n\n  const [isTesting, setIsTesting] = useState(false);\n\n  const handleProviderPick = (value: string) => {\n    formInst.setFieldValue(\"provider\", value);\n  };\n\n  const handleFormChange = () => {\n    setFormChanged(true);\n  };\n\n  const handleOkClick = async () => {\n    await submitForm();\n    setOpen(false);\n  };\n\n  const handleOkAndContinueClick = async () => {\n    await submitForm();\n    message.success(t(\"common.text.saved\"));\n  };\n\n  const handleCancelClick = () => {\n    if (formPending) return;\n\n    setOpen(false);\n  };\n\n  const handleTestPushClick = async () => {\n    setIsTesting(true);\n\n    try {\n      await formInst.validateFields();\n    } catch {\n      setIsTesting(false);\n      return;\n    }\n\n    try {\n      await testPushNotification({ provider: fieldProvider, accessId: data!.id });\n      message.success(t(\"common.text.operation_succeeded\"));\n    } catch (err) {\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n    } finally {\n      setIsTesting(false);\n    }\n  };\n\n  return (\n    <>\n      {triggerEl}\n\n      <Drawer\n        afterOpenChange={(open) => !open && afterClose?.()}\n        autoFocus\n        closeIcon={false}\n        destroyOnHidden\n        footer={\n          fieldProvider ? (\n            <Flex className=\"px-2\" justify=\"space-between\">\n              {usage === \"notification\" ? (\n                <Button className=\"max-sm:invisible\" disabled={mode !== \"modify\" || formChanged} loading={isTesting} onClick={handleTestPushClick}>\n                  {t(\"access.action.test_notify.button\")}\n                </Button>\n              ) : (\n                <span>{/* TODO: 测试连接 */}</span>\n              )}\n              <Flex justify=\"end\" gap=\"small\">\n                <Button disabled={isTesting} onClick={handleCancelClick}>\n                  {t(\"common.button.cancel\")}\n                </Button>\n                <Space.Compact>\n                  <Button disabled={isTesting} loading={formPending} type=\"primary\" onClick={handleOkClick}>\n                    {mode === \"modify\" ? t(\"common.button.save\") : t(\"common.button.submit\")}\n                  </Button>\n                  <Show when={mode === \"modify\"}>\n                    <Dropdown\n                      menu={{\n                        items: [\n                          {\n                            key: \"save_and_continue\",\n                            label: t(\"common.button.save_and_continue\"),\n                            onClick: handleOkAndContinueClick,\n                          },\n                        ],\n                      }}\n                      placement=\"bottomRight\"\n                      trigger={[\"click\"]}\n                    >\n                      <Button disabled={formPending || isTesting} icon={<IconChevronDown size=\"1.25em\" />} type=\"primary\" />\n                    </Dropdown>\n                  </Show>\n                </Space.Compact>\n              </Flex>\n            </Flex>\n          ) : (\n            false\n          )\n        }\n        loading={loading}\n        maskClosable={!formPending}\n        open={open}\n        size=\"large\"\n        title={\n          <Flex align=\"center\" justify=\"space-between\" gap=\"small\">\n            <div className=\"flex-1 truncate\">\n              {mode === \"modify\" && !!data?.id ? t(\"access.action.modify.modal.title\") + ` #${data.id}` : t(`access.action.${mode}.modal.title`)}\n            </div>\n            <Button\n              className=\"ant-drawer-close\"\n              style={{ marginInline: 0 }}\n              icon={<IconX size=\"1.25em\" />}\n              size=\"small\"\n              type=\"text\"\n              onClick={handleCancelClick}\n            />\n          </Flex>\n        }\n        onClose={handleCancelClick}\n      >\n        <Show when={!fieldProvider && !data?.provider}>\n          <AccessProviderPicker\n            gap=\"large\"\n            placeholder={t(\"access.form.provider.search.placeholder\")}\n            showOptionTags={\n              usage == null ||\n              (usage === \"dns-hosting\" ? { [\"builtin\"]: true, [ACCESS_USAGES.DNS]: true, [ACCESS_USAGES.HOSTING]: true } : { [\"builtin\"]: true })\n            }\n            showSearch\n            onFilter={providerFilter}\n            onSelect={handleProviderPick}\n          />\n        </Show>\n\n        <div style={{ display: fieldProvider || data?.provider ? \"block\" : \"none\" }}>\n          <AccessForm form={formInst} disabled={formPending} initialValues={data} mode={mode} usage={usage} onFormValuesChange={handleFormChange} />\n        </div>\n      </Drawer>\n    </>\n  );\n};\n\nconst useDrawer = () => {\n  type DataType = AccessEditDrawerProps[\"data\"];\n  const [data, setData, getData] = useGetState<DataType>();\n  const [loading, setLoading] = useState<boolean>();\n  const [open, setOpen] = useState(false);\n\n  const onOpenChange = useCallback((open: boolean) => {\n    setOpen(open);\n  }, []);\n\n  return {\n    drawerProps: {\n      afterClose: () => {\n        startTransition(() => {\n          if (!open) {\n            setData(void 0);\n            setLoading(void 0);\n          }\n        });\n      },\n      data,\n      loading,\n      open,\n      onOpenChange,\n    },\n\n    open: ({ data, loading }: { data: NonNullable<DataType>; loading?: boolean }) => {\n      setData(data);\n      setLoading(loading);\n      setOpen(true);\n\n      return {\n        safeUpdate: ({ data, loading }: { data?: NonNullable<DataType>; loading?: boolean }) => {\n          if (data != null) {\n            if (data.id !== getData()?.id) return; // 确保数据不脏读\n\n            setData(data);\n          }\n\n          if (loading != null) {\n            setLoading(loading);\n          }\n        },\n      };\n    },\n    close: () => {\n      setOpen(false);\n    },\n  };\n};\n\nconst _default = Object.assign(AccessEditDrawer, {\n  useDrawer,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/AccessForm.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { Form, type FormInstance, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport AccessProviderSelect from \"@/components/provider/AccessProviderSelect\";\nimport { type AccessModel } from \"@/domain/access\";\nimport { ACCESS_PROVIDERS, ACCESS_USAGES } from \"@/domain/provider\";\nimport { useAntdForm } from \"@/hooks\";\n\nimport { FormNestedFieldsContextProvider } from \"./forms/_context\";\nimport { useProviderFilterByUsage } from \"./forms/_hooks\";\nimport AccessConfigFieldsProvider from \"./forms/AccessConfigFieldsProvider\";\n\nexport type AccessFormModes = \"create\" | \"modify\";\nexport type AccessFormUsages = \"dns\" | \"hosting\" | \"dns-hosting\" | \"ca\" | \"notification\";\n\nexport interface AccessFormProps {\n  className?: string;\n  style?: React.CSSProperties;\n  disabled?: boolean;\n  initialValues?: Nullish<MaybeModelRecord<AccessModel>>;\n  form: FormInstance;\n  mode: AccessFormModes;\n  usage?: AccessFormUsages;\n  onFormValuesChange?: (changedValues: Nullish<MaybeModelRecord<AccessModel>>, values: Nullish<MaybeModelRecord<AccessModel>>) => void;\n}\n\nconst AccessForm = ({ className, style, disabled, initialValues, mode, usage, onFormValuesChange, ...props }: AccessFormProps) => {\n  const { t } = useTranslation();\n\n  const providerFilter = useProviderFilterByUsage(usage);\n\n  const formSchema = z.object({\n    name: z\n      .string(t(\"access.form.name.placeholder\"))\n      .min(1, t(\"access.form.name.placeholder\"))\n      .max(64, t(\"common.errmsg.string_max\", { max: 64 })),\n    provider: z.enum(ACCESS_PROVIDERS, t(\"access.form.provider.placeholder\")),\n    config: z.any(),\n    reserve: z.string().nullish(),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const { form: formInst, formProps } = useAntdForm<z.infer<typeof formSchema>>({\n    form: props.form,\n    name: \"accessForm\",\n    initialValues: initialValues,\n  });\n\n  const fieldProvider = Form.useWatch(\"provider\", { form: formInst, preserve: true });\n\n  const renderNestedFieldProviderComponent = AccessConfigFieldsProvider.useComponent(fieldProvider, {\n    initProps: (provider) => {\n      let props: object = { disabled: disabled };\n\n      switch (provider) {\n        case ACCESS_PROVIDERS.WEBHOOK:\n          {\n            props = {\n              ...props,\n              usage: usage === \"notification\" ? \"notification\" : usage === \"hosting\" || usage === \"dns-hosting\" ? \"deployment\" : \"none\",\n            };\n          }\n          break;\n      }\n\n      return props;\n    },\n    deps: [disabled, usage],\n  });\n\n  return (\n    <Form\n      className={className}\n      style={style}\n      {...formProps}\n      clearOnDestroy={true}\n      disabled={disabled}\n      form={formInst}\n      layout=\"vertical\"\n      preserve={false}\n      scrollToFirstError\n      onValuesChange={onFormValuesChange}\n    >\n      <Form.Item name=\"name\" label={t(\"access.form.name.label\")} rules={[formRule]}>\n        <Input placeholder={t(\"access.form.name.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name=\"provider\"\n        label={t(\"access.form.provider.label\")}\n        extra={usage === \"dns-hosting\" ? <span dangerouslySetInnerHTML={{ __html: t(\"access.form.provider.help\") }}></span> : null}\n        rules={[formRule]}\n      >\n        <AccessProviderSelect\n          disabled={mode !== \"create\"}\n          placeholder={t(\"access.form.provider.placeholder\")}\n          showOptionTags={\n            usage == null || (usage === \"dns-hosting\" ? { [\"builtin\"]: true, [ACCESS_USAGES.DNS]: true, [ACCESS_USAGES.HOSTING]: true } : { [\"builtin\"]: true })\n          }\n          showSearch={!disabled}\n          onFilter={providerFilter}\n        />\n      </Form.Item>\n\n      <FormNestedFieldsContextProvider value={{ parentNamePath: \"config\" }}>\n        {renderNestedFieldProviderComponent && <>{renderNestedFieldProviderComponent}</>}\n      </FormNestedFieldsContextProvider>\n    </Form>\n  );\n};\n\nconst _default = Object.assign(AccessForm, {\n  useProviderFilterByUsage,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/AccessSelect.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { useMount } from \"ahooks\";\nimport { Avatar, Select, type SelectProps, Typography, theme } from \"antd\";\n\nimport { type AccessModel } from \"@/domain/access\";\nimport { accessProvidersMap } from \"@/domain/provider\";\nimport { useZustandShallowSelector } from \"@/hooks\";\nimport { useAccessesStore } from \"@/stores/access\";\nimport { matchSearchOption } from \"@/utils/search\";\n\nexport interface AccessTypeSelectProps extends Omit<SelectProps, \"labelRender\" | \"loading\" | \"options\" | \"optionLabelProp\" | \"optionRender\"> {\n  onFilter?: (value: string, option: AccessModel) => boolean;\n}\n\nconst AccessSelect = ({ onFilter, ...props }: AccessTypeSelectProps) => {\n  const { token: themeToken } = theme.useToken();\n\n  const { accesses, loadedAtOnce, fetchAccesses } = useAccessesStore(useZustandShallowSelector([\"accesses\", \"loadedAtOnce\", \"fetchAccesses\"]));\n  useMount(() => {\n    fetchAccesses(false);\n  });\n\n  const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: AccessModel }>>([]);\n  useEffect(() => {\n    const filteredItems = onFilter != null ? accesses.filter((item) => onFilter(item.id, item)) : accesses;\n    setOptions(\n      filteredItems.map((item) => ({\n        key: item.id,\n        value: item.id,\n        label: item.name,\n        data: item,\n      }))\n    );\n  }, [accesses, onFilter]);\n\n  const renderOption = (key: string) => {\n    const access = accesses.find((e) => e.id === key);\n    if (!access) {\n      return (\n        <div className=\"flex items-center gap-2 truncate overflow-hidden\">\n          <Avatar shape=\"square\" size=\"small\" />\n          <Typography.Text ellipsis>{key}</Typography.Text>\n        </div>\n      );\n    }\n\n    const provider = accessProvidersMap.get(access.provider);\n    return (\n      <div className=\"flex items-center gap-2 truncate overflow-hidden\">\n        <Avatar shape=\"square\" src={provider?.icon} size=\"small\" />\n        <Typography.Text ellipsis>{access.name}</Typography.Text>\n      </div>\n    );\n  };\n\n  return (\n    <Select\n      {...props}\n      showSearch={{\n        filterOption: (inputValue, option) => matchSearchOption(inputValue, option!),\n        optionFilterProp: \"label\",\n      }}\n      labelRender={({ value }) => {\n        if (value != null) {\n          return renderOption(value as string);\n        }\n\n        return <span style={{ color: themeToken.colorTextPlaceholder }}>{props.placeholder}</span>;\n      }}\n      loading={!loadedAtOnce}\n      options={options}\n      optionLabelProp={void 0}\n      optionRender={(option) => renderOption(option.data.value)}\n    />\n  );\n};\n\nexport default AccessSelect;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProvider.tsx",
    "content": "﻿import { useEffect, useState } from \"react\";\r\n\r\nimport { ACCESS_PROVIDERS, type AccessProviderType } from \"@/domain/provider\";\r\n\r\nimport AccessConfigFieldsProvider1Panel from \"./AccessConfigFieldsProvider1Panel\";\r\nimport AccessConfigFieldsProvider35cn from \"./AccessConfigFieldsProvider35cn\";\r\nimport AccessConfigFieldsProvider51DNScom from \"./AccessConfigFieldsProvider51DNScom\";\r\nimport AccessConfigFieldsProviderACMECA from \"./AccessConfigFieldsProviderACMECA\";\r\nimport AccessConfigFieldsProviderACMEDNS from \"./AccessConfigFieldsProviderACMEDNS\";\r\nimport AccessConfigFieldsProviderACMEHttpReq from \"./AccessConfigFieldsProviderACMEHttpReq\";\r\nimport AccessConfigFieldsProviderActalisSSL from \"./AccessConfigFieldsProviderActalisSSL\";\r\nimport AccessConfigFieldsProviderAkamai from \"./AccessConfigFieldsProviderAkamai\";\r\nimport AccessConfigFieldsProviderAliyun from \"./AccessConfigFieldsProviderAliyun\";\r\nimport AccessConfigFieldsProviderAPISIX from \"./AccessConfigFieldsProviderAPISIX\";\r\nimport AccessConfigFieldsProviderArvanCloud from \"./AccessConfigFieldsProviderArvanCloud\";\r\nimport AccessConfigFieldsProviderAWS from \"./AccessConfigFieldsProviderAWS\";\r\nimport AccessConfigFieldsProviderAzure from \"./AccessConfigFieldsProviderAzure\";\r\nimport AccessConfigFieldsProviderBaiduCloud from \"./AccessConfigFieldsProviderBaiduCloud\";\r\nimport AccessConfigFieldsProviderBaishan from \"./AccessConfigFieldsProviderBaishan\";\r\nimport AccessConfigFieldsProviderBaotaPanel from \"./AccessConfigFieldsProviderBaotaPanel\";\r\nimport AccessConfigFieldsProviderBaotaPanelGo from \"./AccessConfigFieldsProviderBaotaPanelGo\";\r\nimport AccessConfigFieldsProviderBaotaWAF from \"./AccessConfigFieldsProviderBaotaWAF\";\r\nimport AccessConfigFieldsProviderBookMyName from \"./AccessConfigFieldsProviderBookMyName\";\r\nimport AccessConfigFieldsProviderBunny from \"./AccessConfigFieldsProviderBunny\";\r\nimport AccessConfigFieldsProviderBytePlus from \"./AccessConfigFieldsProviderBytePlus\";\r\nimport AccessConfigFieldsProviderCacheFly from \"./AccessConfigFieldsProviderCacheFly\";\r\nimport AccessConfigFieldsProviderCdnfly from \"./AccessConfigFieldsProviderCdnfly\";\r\nimport AccessConfigFieldsProviderCloudflare from \"./AccessConfigFieldsProviderCloudflare\";\r\nimport AccessConfigFieldsProviderClouDNS from \"./AccessConfigFieldsProviderClouDNS\";\r\nimport AccessConfigFieldsProviderCMCCCloud from \"./AccessConfigFieldsProviderCMCCCloud\";\r\nimport AccessConfigFieldsProviderConstellix from \"./AccessConfigFieldsProviderConstellix\";\r\nimport AccessConfigFieldsProviderCPanel from \"./AccessConfigFieldsProviderCPanel\";\r\nimport AccessConfigFieldsProviderCTCCCloud from \"./AccessConfigFieldsProviderCTCCCloud\";\r\nimport AccessConfigFieldsProviderDeSEC from \"./AccessConfigFieldsProviderDeSEC\";\r\nimport AccessConfigFieldsProviderDigiCert from \"./AccessConfigFieldsProviderDigiCert\";\r\nimport AccessConfigFieldsProviderDigitalOcean from \"./AccessConfigFieldsProviderDigitalOcean\";\r\nimport AccessConfigFieldsProviderDingTalkBot from \"./AccessConfigFieldsProviderDingTalkBot\";\r\nimport AccessConfigFieldsProviderDiscordBot from \"./AccessConfigFieldsProviderDiscordBot\";\r\nimport AccessConfigFieldsProviderDNSExit from \"./AccessConfigFieldsProviderDNSExit\";\r\nimport AccessConfigFieldsProviderDNSLA from \"./AccessConfigFieldsProviderDNSLA\";\r\nimport AccessConfigFieldsProviderDNSMadeEasy from \"./AccessConfigFieldsProviderDNSMadeEasy\";\r\nimport AccessConfigFieldsProviderDogeCloud from \"./AccessConfigFieldsProviderDogeCloud\";\r\nimport AccessConfigFieldsProviderDokploy from \"./AccessConfigFieldsProviderDokploy\";\r\nimport AccessConfigFieldsProviderDuckDNS from \"./AccessConfigFieldsProviderDuckDNS\";\r\nimport AccessConfigFieldsProviderDynu from \"./AccessConfigFieldsProviderDynu\";\r\nimport AccessConfigFieldsProviderDynv6 from \"./AccessConfigFieldsProviderDynv6\";\r\nimport AccessConfigFieldsProviderEmail from \"./AccessConfigFieldsProviderEmail\";\r\nimport AccessConfigFieldsProviderFlexCDN from \"./AccessConfigFieldsProviderFlexCDN\";\r\nimport AccessConfigFieldsProviderFlyIO from \"./AccessConfigFieldsProviderFlyIO\";\r\nimport AccessConfigFieldsProviderGandinet from \"./AccessConfigFieldsProviderGandinet\";\r\nimport AccessConfigFieldsProviderGcore from \"./AccessConfigFieldsProviderGcore\";\r\nimport AccessConfigFieldsProviderGlobalSignAtlas from \"./AccessConfigFieldsProviderGlobalSignAtlas\";\r\nimport AccessConfigFieldsProviderGname from \"./AccessConfigFieldsProviderGname\";\r\nimport AccessConfigFieldsProviderGoDaddy from \"./AccessConfigFieldsProviderGoDaddy\";\r\nimport AccessConfigFieldsProviderGoEdge from \"./AccessConfigFieldsProviderGoEdge\";\r\nimport AccessConfigFieldsProviderGoogleTrustServices from \"./AccessConfigFieldsProviderGoogleTrustServices\";\r\nimport AccessConfigFieldsProviderHetzner from \"./AccessConfigFieldsProviderHetzner\";\r\nimport AccessConfigFieldsProviderHostingde from \"./AccessConfigFieldsProviderHostingde\";\r\nimport AccessConfigFieldsProviderHostinger from \"./AccessConfigFieldsProviderHostinger\";\r\nimport AccessConfigFieldsProviderHuaweiCloud from \"./AccessConfigFieldsProviderHuaweiCloud\";\r\nimport AccessConfigFieldsProviderInfomaniak from \"./AccessConfigFieldsProviderInfomaniak\";\r\nimport AccessConfigFieldsProviderIONOS from \"./AccessConfigFieldsProviderIONOS\";\r\nimport AccessConfigFieldsProviderJDCloud from \"./AccessConfigFieldsProviderJDCloud\";\r\nimport AccessConfigFieldsProviderKong from \"./AccessConfigFieldsProviderKong\";\r\nimport AccessConfigFieldsProviderKsyun from \"./AccessConfigFieldsProviderKsyun\";\r\nimport AccessConfigFieldsProviderKubernetes from \"./AccessConfigFieldsProviderKubernetes\";\r\nimport AccessConfigFieldsProviderLarkBot from \"./AccessConfigFieldsProviderLarkBot\";\r\nimport AccessConfigFieldsProviderLeCDN from \"./AccessConfigFieldsProviderLeCDN\";\r\nimport AccessConfigFieldsProviderLinode from \"./AccessConfigFieldsProviderLinode\";\r\nimport AccessConfigFieldsProviderLiteSSL from \"./AccessConfigFieldsProviderLiteSSL\";\r\nimport AccessConfigFieldsProviderMattermost from \"./AccessConfigFieldsProviderMattermost\";\r\nimport AccessConfigFieldsProviderMohua from \"./AccessConfigFieldsProviderMohua\";\r\nimport AccessConfigFieldsProviderNamecheap from \"./AccessConfigFieldsProviderNamecheap\";\r\nimport AccessConfigFieldsProviderNameDotCom from \"./AccessConfigFieldsProviderNameDotCom\";\r\nimport AccessConfigFieldsProviderNameSilo from \"./AccessConfigFieldsProviderNameSilo\";\r\nimport AccessConfigFieldsProviderNetcup from \"./AccessConfigFieldsProviderNetcup\";\r\nimport AccessConfigFieldsProviderNetlify from \"./AccessConfigFieldsProviderNetlify\";\r\nimport AccessConfigFieldsProviderNginxProxyManager from \"./AccessConfigFieldsProviderNginxProxyManager\";\r\nimport AccessConfigFieldsProviderNS1 from \"./AccessConfigFieldsProviderNS1\";\r\nimport AccessConfigFieldsProviderOVHcloud from \"./AccessConfigFieldsProviderOVHcloud\";\r\nimport AccessConfigFieldsProviderPorkbun from \"./AccessConfigFieldsProviderPorkbun\";\r\nimport AccessConfigFieldsProviderPowerDNS from \"./AccessConfigFieldsProviderPowerDNS\";\r\nimport AccessConfigFieldsProviderProxmoxVE from \"./AccessConfigFieldsProviderProxmoxVE\";\r\nimport AccessConfigFieldsProviderQingCloud from \"./AccessConfigFieldsProviderQingCloud\";\r\nimport AccessConfigFieldsProviderQiniu from \"./AccessConfigFieldsProviderQiniu\";\r\nimport AccessConfigFieldsProviderRainYun from \"./AccessConfigFieldsProviderRainYun\";\r\nimport AccessConfigFieldsProviderRatPanel from \"./AccessConfigFieldsProviderRatPanel\";\r\nimport AccessConfigFieldsProviderRFC2136 from \"./AccessConfigFieldsProviderRFC2136\";\r\nimport AccessConfigFieldsProviderS3 from \"./AccessConfigFieldsProviderS3\";\r\nimport AccessConfigFieldsProviderSafeLine from \"./AccessConfigFieldsProviderSafeLine\";\r\nimport AccessConfigFieldsProviderSectigo from \"./AccessConfigFieldsProviderSectigo\";\r\nimport AccessConfigFieldsProviderSlackBot from \"./AccessConfigFieldsProviderSlackBot\";\r\nimport AccessConfigFieldsProviderSpaceship from \"./AccessConfigFieldsProviderSpaceship\";\r\nimport AccessConfigFieldsProviderSSH from \"./AccessConfigFieldsProviderSSH\";\r\nimport AccessConfigFieldsProviderSSLCom from \"./AccessConfigFieldsProviderSSLCom\";\r\nimport AccessConfigFieldsProviderSynologyDSM from \"./AccessConfigFieldsProviderSynologyDSM\";\r\nimport AccessConfigFieldsProviderTechnitiumDNS from \"./AccessConfigFieldsProviderTechnitiumDNS\";\r\nimport AccessConfigFieldsProviderTelegramBot from \"./AccessConfigFieldsProviderTelegramBot\";\r\nimport AccessConfigFieldsProviderTencentCloud from \"./AccessConfigFieldsProviderTencentCloud\";\r\nimport AccessConfigFieldsProviderTodayNIC from \"./AccessConfigFieldsProviderTodayNIC\";\r\nimport AccessConfigFieldsProviderUCloud from \"./AccessConfigFieldsProviderUCloud\";\r\nimport AccessConfigFieldsProviderUniCloud from \"./AccessConfigFieldsProviderUniCloud\";\r\nimport AccessConfigFieldsProviderUpyun from \"./AccessConfigFieldsProviderUpyun\";\r\nimport AccessConfigFieldsProviderVercel from \"./AccessConfigFieldsProviderVercel\";\r\nimport AccessConfigFieldsProviderVolcEngine from \"./AccessConfigFieldsProviderVolcEngine\";\r\nimport AccessConfigFieldsProviderVultr from \"./AccessConfigFieldsProviderVultr\";\r\nimport AccessConfigFieldsProviderWangsu from \"./AccessConfigFieldsProviderWangsu\";\r\nimport AccessConfigFieldsProviderWebhook from \"./AccessConfigFieldsProviderWebhook\";\r\nimport AccessConfigFieldsProviderWeComBot from \"./AccessConfigFieldsProviderWeComBot\";\r\nimport AccessConfigFieldsProviderWestcn from \"./AccessConfigFieldsProviderWestcn\";\r\nimport AccessConfigFieldsProviderXinnet from \"./AccessConfigFieldsProviderXinnet\";\r\nimport AccessConfigFieldsProviderZeroSSL from \"./AccessConfigFieldsProviderZeroSSL\";\r\n\r\nconst providerComponentMap: Partial<Record<AccessProviderType, React.ComponentType<any>>> = {\r\n  /*\r\n    注意：如果追加新的子组件，请保持以 ASCII 排序。\r\n    NOTICE: If you add new child component, please keep ASCII order.\r\n    */\r\n  [ACCESS_PROVIDERS[\"1PANEL\"]]: AccessConfigFieldsProvider1Panel,\r\n  [ACCESS_PROVIDERS[\"35CN\"]]: AccessConfigFieldsProvider35cn,\r\n  [ACCESS_PROVIDERS[\"51DNSCOM\"]]: AccessConfigFieldsProvider51DNScom,\r\n  [ACCESS_PROVIDERS.ACMECA]: AccessConfigFieldsProviderACMECA,\r\n  [ACCESS_PROVIDERS.ACMEDNS]: AccessConfigFieldsProviderACMEDNS,\r\n  [ACCESS_PROVIDERS.ACMEHTTPREQ]: AccessConfigFieldsProviderACMEHttpReq,\r\n  [ACCESS_PROVIDERS.ACTALISSSL]: AccessConfigFieldsProviderActalisSSL,\r\n  [ACCESS_PROVIDERS.AKAMAI]: AccessConfigFieldsProviderAkamai,\r\n  [ACCESS_PROVIDERS.ALIYUN]: AccessConfigFieldsProviderAliyun,\r\n  [ACCESS_PROVIDERS.APISIX]: AccessConfigFieldsProviderAPISIX,\r\n  [ACCESS_PROVIDERS.ARVANCLOUD]: AccessConfigFieldsProviderArvanCloud,\r\n  [ACCESS_PROVIDERS.AWS]: AccessConfigFieldsProviderAWS,\r\n  [ACCESS_PROVIDERS.AZURE]: AccessConfigFieldsProviderAzure,\r\n  [ACCESS_PROVIDERS.BAIDUCLOUD]: AccessConfigFieldsProviderBaiduCloud,\r\n  [ACCESS_PROVIDERS.BAISHAN]: AccessConfigFieldsProviderBaishan,\r\n  [ACCESS_PROVIDERS.BAOTAPANEL]: AccessConfigFieldsProviderBaotaPanel,\r\n  [ACCESS_PROVIDERS.BAOTAPANELGO]: AccessConfigFieldsProviderBaotaPanelGo,\r\n  [ACCESS_PROVIDERS.BAOTAWAF]: AccessConfigFieldsProviderBaotaWAF,\r\n  [ACCESS_PROVIDERS.BOOKMYNAME]: AccessConfigFieldsProviderBookMyName,\r\n  [ACCESS_PROVIDERS.BUNNY]: AccessConfigFieldsProviderBunny,\r\n  [ACCESS_PROVIDERS.BYTEPLUS]: AccessConfigFieldsProviderBytePlus,\r\n  [ACCESS_PROVIDERS.CACHEFLY]: AccessConfigFieldsProviderCacheFly,\r\n  [ACCESS_PROVIDERS.CDNFLY]: AccessConfigFieldsProviderCdnfly,\r\n  [ACCESS_PROVIDERS.CLOUDFLARE]: AccessConfigFieldsProviderCloudflare,\r\n  [ACCESS_PROVIDERS.CLOUDNS]: AccessConfigFieldsProviderClouDNS,\r\n  [ACCESS_PROVIDERS.CMCCCLOUD]: AccessConfigFieldsProviderCMCCCloud,\r\n  [ACCESS_PROVIDERS.CONSTELLIX]: AccessConfigFieldsProviderConstellix,\r\n  [ACCESS_PROVIDERS.CPANEL]: AccessConfigFieldsProviderCPanel,\r\n  [ACCESS_PROVIDERS.CTCCCLOUD]: AccessConfigFieldsProviderCTCCCloud,\r\n  [ACCESS_PROVIDERS.DESEC]: AccessConfigFieldsProviderDeSEC,\r\n  [ACCESS_PROVIDERS.DIGICERT]: AccessConfigFieldsProviderDigiCert,\r\n  [ACCESS_PROVIDERS.DIGITALOCEAN]: AccessConfigFieldsProviderDigitalOcean,\r\n  [ACCESS_PROVIDERS.DINGTALKBOT]: AccessConfigFieldsProviderDingTalkBot,\r\n  [ACCESS_PROVIDERS.DISCORDBOT]: AccessConfigFieldsProviderDiscordBot,\r\n  [ACCESS_PROVIDERS.DNSEXIT]: AccessConfigFieldsProviderDNSExit,\r\n  [ACCESS_PROVIDERS.DNSLA]: AccessConfigFieldsProviderDNSLA,\r\n  [ACCESS_PROVIDERS.DNSMADEEASY]: AccessConfigFieldsProviderDNSMadeEasy,\r\n  [ACCESS_PROVIDERS.DOKPLOY]: AccessConfigFieldsProviderDokploy,\r\n  [ACCESS_PROVIDERS.DOGECLOUD]: AccessConfigFieldsProviderDogeCloud,\r\n  [ACCESS_PROVIDERS.DUCKDNS]: AccessConfigFieldsProviderDuckDNS,\r\n  [ACCESS_PROVIDERS.DYNU]: AccessConfigFieldsProviderDynu,\r\n  [ACCESS_PROVIDERS.DYNV6]: AccessConfigFieldsProviderDynv6,\r\n  [ACCESS_PROVIDERS.EMAIL]: AccessConfigFieldsProviderEmail,\r\n  [ACCESS_PROVIDERS.FLEXCDN]: AccessConfigFieldsProviderFlexCDN,\r\n  [ACCESS_PROVIDERS.FLYIO]: AccessConfigFieldsProviderFlyIO,\r\n  [ACCESS_PROVIDERS.GANDINET]: AccessConfigFieldsProviderGandinet,\r\n  [ACCESS_PROVIDERS.GCORE]: AccessConfigFieldsProviderGcore,\r\n  [ACCESS_PROVIDERS.GNAME]: AccessConfigFieldsProviderGname,\r\n  [ACCESS_PROVIDERS.GODADDY]: AccessConfigFieldsProviderGoDaddy,\r\n  [ACCESS_PROVIDERS.GOEDGE]: AccessConfigFieldsProviderGoEdge,\r\n  [ACCESS_PROVIDERS.GLOBALSIGNATLAS]: AccessConfigFieldsProviderGlobalSignAtlas,\r\n  [ACCESS_PROVIDERS.GOOGLETRUSTSERVICES]: AccessConfigFieldsProviderGoogleTrustServices,\r\n  [ACCESS_PROVIDERS.HETZNER]: AccessConfigFieldsProviderHetzner,\r\n  [ACCESS_PROVIDERS.HOSTINGDE]: AccessConfigFieldsProviderHostingde,\r\n  [ACCESS_PROVIDERS.HOSTINGER]: AccessConfigFieldsProviderHostinger,\r\n  [ACCESS_PROVIDERS.HUAWEICLOUD]: AccessConfigFieldsProviderHuaweiCloud,\r\n  [ACCESS_PROVIDERS.IONOS]: AccessConfigFieldsProviderIONOS,\r\n  [ACCESS_PROVIDERS.JDCLOUD]: AccessConfigFieldsProviderJDCloud,\r\n  [ACCESS_PROVIDERS.KONG]: AccessConfigFieldsProviderKong,\r\n  [ACCESS_PROVIDERS.KUBERNETES]: AccessConfigFieldsProviderKubernetes,\r\n  [ACCESS_PROVIDERS.KSYUN]: AccessConfigFieldsProviderKsyun,\r\n  [ACCESS_PROVIDERS.LARKBOT]: AccessConfigFieldsProviderLarkBot,\r\n  [ACCESS_PROVIDERS.LECDN]: AccessConfigFieldsProviderLeCDN,\r\n  [ACCESS_PROVIDERS.INFOMANIAK]: AccessConfigFieldsProviderInfomaniak,\r\n  [ACCESS_PROVIDERS.LINODE]: AccessConfigFieldsProviderLinode,\r\n  [ACCESS_PROVIDERS.LITESSL]: AccessConfigFieldsProviderLiteSSL,\r\n  [ACCESS_PROVIDERS.MATTERMOST]: AccessConfigFieldsProviderMattermost,\r\n  [ACCESS_PROVIDERS.MOHUA]: AccessConfigFieldsProviderMohua,\r\n  [ACCESS_PROVIDERS.NAMECHEAP]: AccessConfigFieldsProviderNamecheap,\r\n  [ACCESS_PROVIDERS.NAMEDOTCOM]: AccessConfigFieldsProviderNameDotCom,\r\n  [ACCESS_PROVIDERS.NAMESILO]: AccessConfigFieldsProviderNameSilo,\r\n  [ACCESS_PROVIDERS.NETCUP]: AccessConfigFieldsProviderNetcup,\r\n  [ACCESS_PROVIDERS.NETLIFY]: AccessConfigFieldsProviderNetlify,\r\n  [ACCESS_PROVIDERS.NGINXPROXYMANAGER]: AccessConfigFieldsProviderNginxProxyManager,\r\n  [ACCESS_PROVIDERS.NS1]: AccessConfigFieldsProviderNS1,\r\n  [ACCESS_PROVIDERS.OVHCLOUD]: AccessConfigFieldsProviderOVHcloud,\r\n  [ACCESS_PROVIDERS.PORKBUN]: AccessConfigFieldsProviderPorkbun,\r\n  [ACCESS_PROVIDERS.POWERDNS]: AccessConfigFieldsProviderPowerDNS,\r\n  [ACCESS_PROVIDERS.PROXMOXVE]: AccessConfigFieldsProviderProxmoxVE,\r\n  [ACCESS_PROVIDERS.QINGCLOUD]: AccessConfigFieldsProviderQingCloud,\r\n  [ACCESS_PROVIDERS.QINIU]: AccessConfigFieldsProviderQiniu,\r\n  [ACCESS_PROVIDERS.RAINYUN]: AccessConfigFieldsProviderRainYun,\r\n  [ACCESS_PROVIDERS.RATPANEL]: AccessConfigFieldsProviderRatPanel,\r\n  [ACCESS_PROVIDERS.RFC2136]: AccessConfigFieldsProviderRFC2136,\r\n  [ACCESS_PROVIDERS.S3]: AccessConfigFieldsProviderS3,\r\n  [ACCESS_PROVIDERS.SAFELINE]: AccessConfigFieldsProviderSafeLine,\r\n  [ACCESS_PROVIDERS.SECTIGO]: AccessConfigFieldsProviderSectigo,\r\n  [ACCESS_PROVIDERS.SLACKBOT]: AccessConfigFieldsProviderSlackBot,\r\n  [ACCESS_PROVIDERS.SPACESHIP]: AccessConfigFieldsProviderSpaceship,\r\n  [ACCESS_PROVIDERS.SSLCOM]: AccessConfigFieldsProviderSSLCom,\r\n  [ACCESS_PROVIDERS.SSH]: AccessConfigFieldsProviderSSH,\r\n  [ACCESS_PROVIDERS.SYNOLOGYDSM]: AccessConfigFieldsProviderSynologyDSM,\r\n  [ACCESS_PROVIDERS.TECHNITIUMDNS]: AccessConfigFieldsProviderTechnitiumDNS,\r\n  [ACCESS_PROVIDERS.TELEGRAMBOT]: AccessConfigFieldsProviderTelegramBot,\r\n  [ACCESS_PROVIDERS.TENCENTCLOUD]: AccessConfigFieldsProviderTencentCloud,\r\n  [ACCESS_PROVIDERS.TODAYNIC]: AccessConfigFieldsProviderTodayNIC,\r\n  [ACCESS_PROVIDERS.UCLOUD]: AccessConfigFieldsProviderUCloud,\r\n  [ACCESS_PROVIDERS.UNICLOUD]: AccessConfigFieldsProviderUniCloud,\r\n  [ACCESS_PROVIDERS.UPYUN]: AccessConfigFieldsProviderUpyun,\r\n  [ACCESS_PROVIDERS.VERCEL]: AccessConfigFieldsProviderVercel,\r\n  [ACCESS_PROVIDERS.VOLCENGINE]: AccessConfigFieldsProviderVolcEngine,\r\n  [ACCESS_PROVIDERS.VULTR]: AccessConfigFieldsProviderVultr,\r\n  [ACCESS_PROVIDERS.WANGSU]: AccessConfigFieldsProviderWangsu,\r\n  [ACCESS_PROVIDERS.WEBHOOK]: AccessConfigFieldsProviderWebhook,\r\n  [ACCESS_PROVIDERS.WECOMBOT]: AccessConfigFieldsProviderWeComBot,\r\n  [ACCESS_PROVIDERS.WESTCN]: AccessConfigFieldsProviderWestcn,\r\n  [ACCESS_PROVIDERS.XINNET]: AccessConfigFieldsProviderXinnet,\r\n  [ACCESS_PROVIDERS.ZEROSSL]: AccessConfigFieldsProviderZeroSSL,\r\n};\r\n\r\nconst useComponent = (provider: string, { initProps, deps = [] }: { initProps?: (provider: string) => any; deps?: unknown[] }) => {\r\n  const initComponent = () => {\r\n    const Component = providerComponentMap[provider as AccessProviderType];\r\n    if (!Component) return null;\r\n\r\n    const props = initProps?.(provider);\r\n    if (props) {\r\n      return <Component {...props} />;\r\n    }\r\n\r\n    return <Component />;\r\n  };\r\n\r\n  const [component, setComponent] = useState(() => initComponent());\r\n\r\n  useEffect(() => setComponent(initComponent()), [provider]);\r\n  useEffect(() => setComponent(initComponent()), deps);\r\n\r\n  return component;\r\n};\r\n\r\nconst _default = {\r\n  useComponent,\r\n};\r\n\r\nexport default _default;\r\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProvider1Panel.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProvider1Panel = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.1panel_server_url.label\")}\n        extra={t(\"access.form.1panel_server_url.help\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.1panel_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiVersion\"]}\n        initialValue={initialValues.apiVersion}\n        label={t(\"access.form.1panel_api_version.label\")}\n        rules={[formRule]}\n      >\n        <Select options={[\"v1\", \"v2\"].map((s) => ({ label: s, value: s }))} placeholder={t(\"access.form.1panel_api_version.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.1panel_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.1panel_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.1panel_api_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:20410/\",\n    apiVersion: \"v1\",\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    apiVersion: z.string().nonempty(t(\"access.form.1panel_api_version.placeholder\")),\n    apiKey: z.string().nonempty(t(\"access.form.1panel_api_key.placeholder\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProvider1Panel, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProvider35cn.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProvider35cn = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"username\"]} initialValue={initialValues.username} label={t(\"access.form.35cn_username.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.35cn_username.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiPassword\"]}\n        initialValue={initialValues.apiPassword}\n        label={t(\"access.form.35cn_api_password.label\")}\n        rules={[formRule]}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.35cn_api_password.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.35cn_agent.guide\") }}></span>} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    username: \"\",\n    apiPassword: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    username: z.string().nonempty(t(\"access.form.35cn_username.placeholder\")),\n    apiPassword: z.string().nonempty(t(\"access.form.35cn_api_password.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProvider35cn, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProvider51DNScom.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProvider51DNScom = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.51dnscom_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.51dnscom_api_key.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.51dnscom_api_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiSecret\"]}\n        initialValue={initialValues.apiSecret}\n        label={t(\"access.form.51dnscom_api_secret.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.51dnscom_api_secret.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.51dnscom_api_secret.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiKey: \"\",\n    apiSecret: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiKey: z.string().nonempty(t(\"access.form.51dnscom_api_key.placeholder\")),\n    apiSecret: z.string().nonempty(t(\"access.form.51dnscom_api_secret.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProvider51DNScom, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderACMECA.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderACMECA = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"endpoint\"]}\n        initialValue={initialValues.endpoint}\n        label={t(\"access.form.acmeca_endpoint.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.acmeca_endpoint.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"access.form.acmeca_endpoint.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"eabKid\"]} initialValue={initialValues.eabKid} label={t(\"access.form.acmeca_eab_kid.label\")} rules={[formRule]}>\n        <Input allowClear autoComplete=\"new-password\" placeholder={t(\"access.form.acmeca_eab_kid.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"eabHmacKey\"]}\n        initialValue={initialValues.eabHmacKey}\n        label={t(\"access.form.acmeca_eab_hmac_key.label\")}\n        rules={[formRule]}\n      >\n        <Input.Password allowClear autoComplete=\"new-password\" placeholder={t(\"access.form.acmeca_eab_hmac_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    endpoint: \"https://example.com/acme/directory\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    endpoint: z.url(t(\"common.errmsg.url_invalid\")),\n    eabKid: z.string().nullish(),\n    eabHmacKey: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderACMECA, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderACMEDNS.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod/v4\";\n\nimport FileTextInput from \"@/components/FileTextInput\";\nimport { isJsonObject } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFieldsProviderACMEDNS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.acmedns_server_url.label\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.acmedns_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"credentials\"]}\n        initialValue={initialValues.credentials}\n        label={t(\"access.form.acmedns_credentials.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.acmedns_credentials.tooltip\") }}></span>}\n      >\n        <FileTextInput autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t(\"access.form.acmedns_credentials.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"https://auth.acme-dns.io/\",\n    credentials: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    credentials: z.string().refine((v) => isJsonObject(v), t(\"common.errmsg.json_invalid\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFieldsProviderACMEDNS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderACMEHttpReq.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderACMEHttpReq = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"endpoint\"]}\n        initialValue={initialValues.endpoint}\n        label={t(\"access.form.acmehttpreq_endpoint.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.acmehttpreq_endpoint.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"access.form.acmehttpreq_endpoint.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"mode\"]}\n        initialValue={initialValues.mode}\n        label={t(\"access.form.acmehttpreq_mode.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.acmehttpreq_mode.tooltip\") }}></span>}\n      >\n        <Select\n          options={[\n            { label: \"(default)\", value: \"\" },\n            { label: \"RAW\", value: \"RAW\" },\n          ]}\n          placeholder={t(\"access.form.acmehttpreq_mode.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"username\"]}\n        initialValue={initialValues.username}\n        label={t(\"access.form.acmehttpreq_username.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.acmehttpreq_username.tooltip\") }}></span>}\n      >\n        <Input allowClear autoComplete=\"new-password\" placeholder={t(\"access.form.acmehttpreq_username.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"password\"]}\n        initialValue={initialValues.password}\n        label={t(\"access.form.acmehttpreq_password.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.acmehttpreq_password.tooltip\") }}></span>}\n      >\n        <Input.Password allowClear autoComplete=\"new-password\" placeholder={t(\"access.form.acmehttpreq_password.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    endpoint: \"https://example.com/api/\",\n    mode: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    endpoint: z.url(t(\"common.errmsg.url_invalid\")),\n    mode: z.string().nullish(),\n    username: z.string().nullish(),\n    password: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderACMEHttpReq, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderAPISIX.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderAPISIX = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.apisix_server_url.label\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.apisix_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.apisix_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.apisix_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.apisix_api_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:9180/\",\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    apiKey: z.string().nonempty(t(\"access.form.apisix_api_key.placeholder\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderAPISIX, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderAWS.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderAWS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessKeyId\"]}\n        initialValue={initialValues.accessKeyId}\n        label={t(\"access.form.aws_access_key_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.aws_access_key_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.aws_access_key_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretAccessKey\"]}\n        initialValue={initialValues.secretAccessKey}\n        label={t(\"access.form.aws_secret_access_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.aws_secret_access_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.aws_secret_access_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessKeyId: \"\",\n    secretAccessKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessKeyId: z.string().nonempty(t(\"access.form.aws_access_key_id.placeholder\")),\n    secretAccessKey: z.string().nonempty(t(\"access.form.aws_secret_access_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderAWS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderActalisSSL.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderActalisSSL = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"eabKid\"]} initialValue={initialValues.eabKid} label={t(\"access.form.shared_acme_eab_kid.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_kid.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"eabHmacKey\"]}\n        initialValue={initialValues.eabHmacKey}\n        label={t(\"access.form.shared_acme_eab_hmac_key.label\")}\n        rules={[formRule]}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_hmac_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.actalisssl_eab.guide\") }}></span>} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    eabKid: \"\",\n    eabHmacKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    eabKid: z.string().nonempty(t(\"access.form.shared_acme_eab_kid.placeholder\")),\n    eabHmacKey: z.string().nonempty(t(\"access.form.shared_acme_eab_hmac_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderActalisSSL, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderAkamai.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderAkamai = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"host\"]}\n        initialValue={initialValues.host}\n        label={t(\"access.form.akamai_host.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.akamai_host.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.akamai_host.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"clientToken\"]}\n        initialValue={initialValues.clientToken}\n        label={t(\"access.form.akamai_client_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.akamai_client_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.akamai_client_token.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"clientSecret\"]}\n        initialValue={initialValues.clientSecret}\n        label={t(\"access.form.akamai_client_secret.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.akamai_client_secret.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.akamai_client_secret.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"accessToken\"]}\n        initialValue={initialValues.accessToken}\n        label={t(\"access.form.akamai_access_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.akamai_access_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.akamai_access_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    host: \"\",\n    clientToken: \"\",\n    clientSecret: \"\",\n    accessToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    host: z.string().nonempty(t(\"access.form.akamai_host.placeholder\")),\n    clientToken: z.string().nonempty(t(\"access.form.akamai_client_token.placeholder\")),\n    clientSecret: z.string().nonempty(t(\"access.form.akamai_client_secret.placeholder\")),\n    accessToken: z.string().nonempty(t(\"access.form.akamai_access_token.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderAkamai, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderAliyun.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderAliyun = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessKeyId\"]}\n        initialValue={initialValues.accessKeyId}\n        label={t(\"access.form.aliyun_access_key_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.aliyun_access_key_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.aliyun_access_key_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"accessKeySecret\"]}\n        initialValue={initialValues.accessKeySecret}\n        label={t(\"access.form.aliyun_access_key_secret.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.aliyun_access_key_secret.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.aliyun_access_key_secret.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceGroupId\"]}\n        initialValue={initialValues.resourceGroupId}\n        label={t(\"access.form.aliyun_resource_group_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.aliyun_resource_group_id.tooltip\") }}></span>}\n      >\n        <Input allowClear autoComplete=\"new-password\" placeholder={t(\"access.form.aliyun_resource_group_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessKeyId: \"\",\n    accessKeySecret: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessKeyId: z.string().nonempty(t(\"access.form.aliyun_access_key_id.placeholder\")),\n    accessKeySecret: z.string().nonempty(t(\"access.form.aliyun_access_key_secret.placeholder\")),\n    resourceGroupId: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderAliyun, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderArvanCloud.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderArvanCloud = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.arvancloud_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.arvancloud_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.arvancloud_api_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiKey: z.string().nonempty(t(\"access.form.arvancloud_api_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderArvanCloud, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderAzure.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { AutoComplete, Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { matchSearchOption } from \"@/utils/search\";\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderAzure = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"tenantId\"]}\n        initialValue={initialValues.tenantId}\n        label={t(\"access.form.azure_tenant_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.azure_tenant_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.azure_tenant_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"clientId\"]}\n        initialValue={initialValues.clientId}\n        label={t(\"access.form.azure_client_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.azure_client_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.azure_client_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"clientSecret\"]}\n        initialValue={initialValues.clientSecret}\n        label={t(\"access.form.azure_client_secret.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.azure_client_secret.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.azure_client_secret.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"subscriptionId\"]}\n        initialValue={initialValues.subscriptionId}\n        label={t(\"access.form.azure_subscription_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.azure_subscription_id.tooltip\") }}></span>}\n      >\n        <Input allowClear autoComplete=\"new-password\" placeholder={t(\"access.form.azure_subscription_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceGroupName\"]}\n        initialValue={initialValues.resourceGroupName}\n        label={t(\"access.form.azure_resource_group_name.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.azure_resource_group_name.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"access.form.azure_resource_group_name.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"cloudName\"]}\n        initialValue={initialValues.cloudName}\n        label={t(\"access.form.azure_cloud_name.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.azure_cloud_name.tooltip\") }}></span>}\n      >\n        <AutoComplete\n          options={[\"public\", \"azureusgovernment\", \"azurechina\"].map((value) => ({ value }))}\n          placeholder={t(\"access.form.azure_cloud_name.placeholder\")}\n          showSearch={{\n            filterOption: (inputValue, option) => matchSearchOption(inputValue, option!),\n          }}\n        />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    tenantId: \"\",\n    clientId: \"\",\n    clientSecret: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    tenantId: z.string().nonempty(t(\"access.form.azure_tenant_id.placeholder\")),\n    clientId: z.string().nonempty(t(\"access.form.azure_client_id.placeholder\")),\n    clientSecret: z.string().nonempty(t(\"access.form.azure_client_secret.placeholder\")),\n    subscriptionId: z.string().nullish(),\n    resourceGroupName: z.string().nullish(),\n    cloudName: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderAzure, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderBaiduCloud.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderBaiduCloud = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessKeyId\"]}\n        initialValue={initialValues.accessKeyId}\n        label={t(\"access.form.baiducloud_access_key_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.baiducloud_access_key_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.baiducloud_access_key_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretAccessKey\"]}\n        initialValue={initialValues.secretAccessKey}\n        label={t(\"access.form.baiducloud_secret_access_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.baiducloud_secret_access_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.baiducloud_secret_access_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessKeyId: \"\",\n    secretAccessKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessKeyId: z.string().nonempty(t(\"access.form.baiducloud_access_key_id.placeholder\")),\n    secretAccessKey: z.string().nonempty(t(\"access.form.baiducloud_secret_access_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderBaiduCloud, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderBaishan.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderBaishan = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"apiToken\"]} initialValue={initialValues.apiToken} label={t(\"access.form.baishan_api_token.label\")} rules={[formRule]}>\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.baishan_api_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiToken: z.string().nonempty(t(\"access.form.baishan_api_token.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderBaishan, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderBaotaPanel.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderBaotaPanel = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.baotapanel_server_url.label\")}\n        extra={t(\"access.form.baotapanel_server_url.help\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.baotapanel_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.baotapanel_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.baotapanel_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.baotapanel_api_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:8888/\",\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    apiKey: z.string().nonempty(t(\"access.form.baotapanel_api_key.placeholder\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderBaotaPanel, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderBaotaPanelGo.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderBaotaPanelGo = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.baotapanelgo_server_url.label\")}\n        extra={t(\"access.form.baotapanelgo_server_url.help\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.baotapanelgo_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.baotapanelgo_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.baotapanelgo_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.baotapanelgo_api_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:8888/\",\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    apiKey: z.string().nonempty(t(\"access.form.baotapanelgo_api_key.placeholder\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderBaotaPanelGo, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderBaotaWAF.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderBaotaWAF = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.baotawaf_server_url.label\")}\n        extra={t(\"access.form.baotawaf_server_url.help\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.baotawaf_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.baotawaf_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.baotawaf_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.baotawaf_api_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:8379/\",\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    apiKey: z.string().nonempty(t(\"access.form.baotawaf_api_key.placeholder\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderBaotaWAF, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderBookMyName.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderBookMyName = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"username\"]}\n        initialValue={initialValues.username}\n        label={t(\"access.form.bookmyname_username.label\")}\n        rules={[formRule]}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.bookmyname_username.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"password\"]}\n        initialValue={initialValues.password}\n        label={t(\"access.form.bookmyname_password.label\")}\n        rules={[formRule]}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.bookmyname_password.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    username: \"\",\n    password: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    username: z.string().nonempty(t(\"access.form.bookmyname_username.placeholder\")),\n    password: z.string().nonempty(t(\"access.form.bookmyname_password.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderBookMyName, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderBunny.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderBunny = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.bunny_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.bunny_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.bunny_api_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiKey: z.string().nonempty(t(\"access.form.bunny_api_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderBunny, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderBytePlus.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderBytePlus = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessKey\"]}\n        initialValue={initialValues.accessKey}\n        label={t(\"access.form.byteplus_access_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.byteplus_access_key.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.byteplus_access_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretKey\"]}\n        initialValue={initialValues.secretKey}\n        label={t(\"access.form.byteplus_secret_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.byteplus_secret_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.byteplus_secret_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessKey: \"\",\n    secretKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessKey: z.string().nonempty(t(\"access.form.byteplus_access_key.placeholder\")),\n    secretKey: z.string().nonempty(t(\"access.form.byteplus_secret_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderBytePlus, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderCMCCCloud.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderCMCCCloud = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessKeyId\"]}\n        initialValue={initialValues.accessKeyId}\n        label={t(\"access.form.cmcccloud_access_key_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.cmcccloud_access_key_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.cmcccloud_access_key_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"accessKeySecret\"]}\n        initialValue={initialValues.accessKeySecret}\n        label={t(\"access.form.cmcccloud_access_key_secret.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.cmcccloud_access_key_secret.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.cmcccloud_access_key_secret.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessKeyId: \"\",\n    accessKeySecret: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessKeyId: z.string().nonempty(t(\"access.form.cmcccloud_access_key_id.placeholder\")),\n    accessKeySecret: z.string().nonempty(t(\"access.form.cmcccloud_access_key_secret.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderCMCCCloud, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderCPanel.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderCPanel = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.cpanel_server_url.label\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.cpanel_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"username\"]} initialValue={initialValues.apiToken} label={t(\"access.form.cpanel_username.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.cpanel_username.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiToken\"]}\n        initialValue={initialValues.apiToken}\n        label={t(\"access.form.cpanel_api_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.cpanel_api_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.cpanel_api_token.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:2082/\",\n    username: \"\",\n    apiToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    username: z.string().nonempty(t(\"access.form.cpanel_username.placeholder\")),\n    apiToken: z.string().nonempty(t(\"access.form.cpanel_api_token.placeholder\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderCPanel, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderCTCCCloud.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderCTCCCloud = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessKeyId\"]}\n        initialValue={initialValues.accessKeyId}\n        label={t(\"access.form.ctcccloud_access_key_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ctcccloud_access_key_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.ctcccloud_access_key_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretAccessKey\"]}\n        initialValue={initialValues.secretAccessKey}\n        label={t(\"access.form.ctcccloud_secret_access_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ctcccloud_secret_access_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.ctcccloud_secret_access_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessKeyId: \"\",\n    secretAccessKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessKeyId: z.string().nonempty(t(\"access.form.ctcccloud_access_key_id.placeholder\")),\n    secretAccessKey: z.string().nonempty(t(\"access.form.ctcccloud_secret_access_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderCTCCCloud, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderCacheFly.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderCacheFly = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"apiToken\"]} initialValue={initialValues.apiToken} label={t(\"access.form.cachefly_api_token.label\")} rules={[formRule]}>\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.cachefly_api_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiToken: z.string().nonempty(t(\"access.form.cachefly_api_token.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderCacheFly, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderCdnfly.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderCdnfly = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.cdnfly_server_url.label\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.cdnfly_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.cdnfly_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.cdnfly_api_key.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.cdnfly_api_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiSecret\"]}\n        initialValue={initialValues.apiSecret}\n        label={t(\"access.form.cdnfly_api_secret.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.cdnfly_api_secret.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.cdnfly_api_secret.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:88/\",\n    apiKey: \"\",\n    apiSecret: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    apiKey: z.string().nonempty(t(\"access.form.cdnfly_api_key.placeholder\")),\n    apiSecret: z.string().nonempty(t(\"access.form.cdnfly_api_secret.placeholder\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderCdnfly, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderClouDNS.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderClouDNS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"authId\"]}\n        initialValue={initialValues.authId}\n        label={t(\"access.form.cloudns_auth_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.cloudns_auth_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.cloudns_auth_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"authPassword\"]}\n        initialValue={initialValues.authPassword}\n        label={t(\"access.form.cloudns_auth_password.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.cloudns_auth_password.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.cloudns_auth_password.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    authId: \"\",\n    authPassword: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    authId: z.string().nonempty(t(\"access.form.cloudns_auth_id.placeholder\")),\n    authPassword: z.string().nonempty(t(\"access.form.cloudns_auth_password.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderClouDNS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderCloudflare.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderCloudflare = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"dnsApiToken\"]}\n        initialValue={initialValues.dnsApiToken}\n        label={t(\"access.form.cloudflare_dns_api_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.cloudflare_dns_api_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.cloudflare_dns_api_token.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"zoneApiToken\"]}\n        initialValue={initialValues.zoneApiToken}\n        label={t(\"access.form.cloudflare_zone_api_token.label\")}\n        extra={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.cloudflare_zone_api_token.help\") }}></span>}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.cloudflare_zone_api_token.tooltip\") }}></span>}\n      >\n        <Input.Password allowClear autoComplete=\"new-password\" placeholder={t(\"access.form.cloudflare_zone_api_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    dnsApiToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    dnsApiToken: z.string().nonempty(t(\"access.form.cloudflare_dns_api_token.placeholder\")),\n    zoneApiToken: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderCloudflare, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderConstellix.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderConstellix = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.constellix_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.constellix_api_key.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.constellix_api_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretKey\"]}\n        initialValue={initialValues.secretKey}\n        label={t(\"access.form.constellix_secret_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.constellix_secret_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.constellix_secret_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiKey: \"\",\n    secretKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiKey: z.string().nonempty(t(\"access.form.constellix_api_key.placeholder\")),\n    secretKey: z.string().nonempty(t(\"access.form.constellix_secret_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderConstellix, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderDNSExit.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\r\nimport { Form, Input } from \"antd\";\r\nimport { createSchemaFieldRule } from \"antd-zod\";\r\nimport { z } from \"zod\";\r\n\r\nimport { useFormNestedFieldsContext } from \"./_context\";\r\n\r\nconst AccessConfigFormFieldsProviderDNSExit = () => {\r\n  const { i18n, t } = useTranslation();\r\n\r\n  const { parentNamePath } = useFormNestedFieldsContext();\r\n  const formSchema = z.object({\r\n    [parentNamePath]: getSchema({ i18n }),\r\n  });\r\n  const formRule = createSchemaFieldRule(formSchema);\r\n  const initialValues = getInitialValues();\r\n\r\n  return (\r\n    <>\r\n      <Form.Item\r\n        name={[parentNamePath, \"apiKey\"]}\r\n        initialValue={initialValues.apiKey}\r\n        label={t(\"access.form.dnsexit_api_key.label\")}\r\n        rules={[formRule]}\r\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.dnsexit_api_key.tooltip\") }}></span>}\r\n      >\r\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.dnsexit_api_key.placeholder\")} />\r\n      </Form.Item>\r\n    </>\r\n  );\r\n};\r\n\r\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\r\n  return {\r\n    apiKey: \"\",\r\n  };\r\n};\r\n\r\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\r\n  const { t } = i18n;\r\n\r\n  return z.object({\r\n    apiKey: z.string().nonempty(t(\"access.form.dnsexit_api_key.placeholder\")),\r\n  });\r\n};\r\n\r\nconst _default = Object.assign(AccessConfigFormFieldsProviderDNSExit, {\r\n  getInitialValues,\r\n  getSchema,\r\n});\r\n\r\nexport default _default;\r\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderDNSLA.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderDNSLA = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiId\"]}\n        initialValue={initialValues.apiId}\n        label={t(\"access.form.dnsla_api_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.dnsla_api_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.dnsla_api_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiSecret\"]}\n        initialValue={initialValues.apiSecret}\n        label={t(\"access.form.dnsla_api_secret.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.dnsla_api_secret.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.dnsla_api_secret.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiId: \"\",\n    apiSecret: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiId: z.string().nonempty(t(\"access.form.dnsla_api_id.placeholder\")),\n    apiSecret: z.string().nonempty(t(\"access.form.dnsla_api_secret.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderDNSLA, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderDNSMadeEasy.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderDNSMadeEasy = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.dnsmadeeasy_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.dnsmadeeasy_api_key.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.dnsmadeeasy_api_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiSecret\"]}\n        initialValue={initialValues.apiSecret}\n        label={t(\"access.form.dnsmadeeasy_api_secret.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.dnsmadeeasy_api_secret.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.dnsmadeeasy_api_secret.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiKey: \"\",\n    apiSecret: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiKey: z.string().nonempty(t(\"access.form.dnsmadeeasy_api_key.placeholder\")),\n    apiSecret: z.string().nonempty(t(\"access.form.dnsmadeeasy_api_secret.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderDNSMadeEasy, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderDeSEC.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderDeSEC = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"token\"]}\n        initialValue={initialValues.token}\n        label={t(\"access.form.desec_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.desec_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.desec_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    token: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    token: z.string().nonempty(t(\"access.form.desec_token.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderDeSEC, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderDigiCert.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderDigiCert = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"eabKid\"]} initialValue={initialValues.eabKid} label={t(\"access.form.shared_acme_eab_kid.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_kid.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"eabHmacKey\"]}\n        initialValue={initialValues.eabHmacKey}\n        label={t(\"access.form.shared_acme_eab_hmac_key.label\")}\n        rules={[formRule]}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_hmac_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.digicert_eab.guide\") }}></span>} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    eabKid: \"\",\n    eabHmacKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    eabKid: z.string().nonempty(t(\"access.form.shared_acme_eab_kid.placeholder\")),\n    eabHmacKey: z.string().nonempty(t(\"access.form.shared_acme_eab_hmac_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderDigiCert, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderDigitalOcean.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderDigitalOcean = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessToken\"]}\n        initialValue={initialValues.accessToken}\n        label={t(\"access.form.digitalocean_access_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.digitalocean_access_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.digitalocean_access_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessToken: z.string().nonempty(t(\"access.form.digitalocean_access_token.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderDigitalOcean, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderDingTalkBot.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Checkbox, Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport CodeTextInput from \"@/components/CodeTextInput\";\nimport { isJsonObject } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderDingTalkBot = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance<z.infer<typeof formSchema>>();\n  const initialValues = getInitialValues();\n\n  const fieldUseCustomPayload = Form.useWatch([parentNamePath, \"useCustomPayload\"], formInst);\n\n  const handleCustomPayloadChecked = (checked: boolean) => {\n    formInst.setFieldValue([parentNamePath, \"useCustomPayload\"], checked);\n    if (checked) {\n      formInst.setFieldValue([parentNamePath, \"customPayload\"], commonPayloadString);\n    } else {\n      formInst.setFieldValue([parentNamePath, \"customPayload\"], void 0);\n    }\n  };\n\n  const handleCustomPayloadBlur = () => {\n    const value = formInst.getFieldValue([parentNamePath, \"customPayload\"]);\n    try {\n      const json = JSON.stringify(JSON.parse(value), null, 2);\n      formInst.setFieldValue([parentNamePath, \"customPayload\"], json);\n    } catch {\n      return;\n    }\n  };\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"webhookUrl\"]}\n        initialValue={initialValues.webhookUrl}\n        label={t(\"access.form.dingtalkbot_webhook_url.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.dingtalkbot_webhook_url.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"access.form.dingtalkbot_webhook_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secret\"]}\n        initialValue={initialValues.secret}\n        label={t(\"access.form.dingtalkbot_secret.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.dingtalkbot_secret.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.dingtalkbot_secret.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item label={t(\"access.form.dingtalkbot_custom_payload.label\")}>\n        <Form.Item name={[parentNamePath, \"useCustomPayload\"]} noStyle>\n          <Checkbox checked={!!fieldUseCustomPayload} onChange={(e) => handleCustomPayloadChecked(e.target.checked)}>\n            {t(\"access.form.dingtalkbot_custom_payload.checkbox\")}\n          </Checkbox>\n        </Form.Item>\n        <Form.Item\n          name={[parentNamePath, \"customPayload\"]}\n          hidden={!fieldUseCustomPayload}\n          initialValue={initialValues.customPayload}\n          noStyle\n          rules={[formRule]}\n        >\n          <CodeTextInput\n            className=\"mt-2\"\n            lineWrapping={false}\n            height=\"auto\"\n            minHeight=\"64px\"\n            maxHeight=\"256px\"\n            language=\"json\"\n            placeholder={t(\"access.form.dingtalkbot_custom_payload.placeholder\")}\n            onBlur={handleCustomPayloadBlur}\n          />\n        </Form.Item>\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    webhookUrl: \"\",\n    secret: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      webhookUrl: z.url(t(\"common.errmsg.url_invalid\")),\n      secret: z.string().nullish(),\n      useCustomPayload: z.boolean().nullish(),\n      customPayload: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.useCustomPayload) {\n        if (!isJsonObject(values.customPayload!)) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: t(\"common.errmsg.json_invalid\"),\n            path: [\"customPayload\"],\n          });\n        }\n      }\n    });\n};\n\nconst commonPayloadString = JSON.stringify(\n  {\n    msgtype: \"text\",\n    text: {\n      content: \"${CERTIMATE_NOTIFIER_SUBJECT}\\n\\n${CERTIMATE_NOTIFIER_MESSAGE}\",\n    },\n  },\n  null,\n  2\n);\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderDingTalkBot, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderDiscordBot.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderDiscordBot = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"botToken\"]}\n        initialValue={initialValues.botToken}\n        label={t(\"access.form.discordbot_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.discordbot_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.discordbot_token.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"channelId\"]}\n        initialValue={initialValues.channelId}\n        label={t(\"access.form.discordbot_channel_id.label\")}\n        extra={t(\"access.form.discordbot_channel_id.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.discordbot_channel_id.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"access.form.discordbot_channel_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    botToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    botToken: z.string().nonempty(t(\"access.form.discordbot_token.placeholder\")),\n    channelId: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderDiscordBot, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderDogeCloud.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderDogeCloud = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessKey\"]}\n        initialValue={initialValues.accessKey}\n        label={t(\"access.form.dogecloud_access_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.dogecloud_access_key.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.dogecloud_access_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretKey\"]}\n        initialValue={initialValues.secretKey}\n        label={t(\"access.form.dogecloud_secret_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.dogecloud_secret_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.dogecloud_secret_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessKey: \"\",\n    secretKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessKey: z.string().nonempty(t(\"access.form.dogecloud_access_key.placeholder\")),\n    secretKey: z.string().nonempty(t(\"access.form.dogecloud_secret_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderDogeCloud, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderDokploy.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderDokploy = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.dokploy_server_url.label\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.dokploy_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.dokploy_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.dokploy_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.dokploy_api_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:3000/\",\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    apiKey: z.string().nonempty(t(\"access.form.dokploy_api_key.placeholder\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderDokploy, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderDuckDNS.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderDuckDNS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"token\"]}\n        initialValue={initialValues.token}\n        label={t(\"access.form.duckdns_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.duckdns_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.duckdns_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    token: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    token: z.string().nonempty(t(\"access.form.duckdns_token.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderDuckDNS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderDynu.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderDynu = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.dynu_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.dynu_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.dynu_api_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiKey: z.string().nonempty(t(\"access.form.dynu_api_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderDynu, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderDynv6.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderDynv6 = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"httpToken\"]}\n        initialValue={initialValues.httpToken}\n        label={t(\"access.form.dynv6_http_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.dynv6_http_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.dynv6_http_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    httpToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    httpToken: z.string().nonempty(t(\"access.form.dynv6_http_token.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderDynv6, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderEmail.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, InputNumber, Select, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isEmail, isHostname, isPortNumber } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderEmail = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldSmtpTls = Form.useWatch<boolean>([parentNamePath, \"smtpTls\"], formInst);\n\n  return (\n    <>\n      <div className=\"flex space-x-2\">\n        <div className=\"w-3/5\">\n          <Form.Item\n            name={[parentNamePath, \"smtpHost\"]}\n            initialValue={initialValues.smtpHost}\n            label={t(\"access.form.email_smtp_host.label\")}\n            rules={[formRule]}\n          >\n            <Input placeholder={t(\"access.form.email_smtp_host.placeholder\")} />\n          </Form.Item>\n        </div>\n\n        <div className=\"w-2/5\">\n          <Form.Item\n            name={[parentNamePath, \"smtpPort\"]}\n            initialValue={initialValues.smtpPort}\n            label={t(\"access.form.email_smtp_port.label\")}\n            rules={[formRule]}\n          >\n            <InputNumber style={{ width: \"100%\" }} placeholder={t(\"access.form.email_smtp_port.placeholder\")} min={1} max={65535} />\n          </Form.Item>\n        </div>\n      </div>\n\n      <div className=\"flex space-x-8\">\n        <div className={fieldSmtpTls ? \"w-1/2\" : \"w-3/5\"}>\n          <Form.Item name={[parentNamePath, \"smtpTls\"]} initialValue={initialValues.smtpTls} label={t(\"access.form.email_smtp_tls.label\")} rules={[formRule]}>\n            <Select placeholder={t(\"access.form.email_smtp_tls.placeholder\")}>\n              <Select.Option value={true}>{t(\"access.form.email_smtp_tls.option.true.label\")}</Select.Option>\n              <Select.Option value={false}>{t(\"access.form.email_smtp_tls.option.false.label\")}</Select.Option>\n            </Select>\n          </Form.Item>\n        </div>\n\n        <Show when={fieldSmtpTls}>\n          <div className=\"w-1/2\">\n            <Form.Item\n              name={[parentNamePath, \"allowInsecureConnections\"]}\n              initialValue={initialValues.allowInsecureConnections}\n              label={t(\"access.form.shared_allow_insecure_conns.label\")}\n              rules={[formRule]}\n            >\n              <Switch />\n            </Form.Item>\n          </div>\n        </Show>\n      </div>\n\n      <Form.Item name={[parentNamePath, \"username\"]} initialValue={initialValues.username} label={t(\"access.form.email_username.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.email_username.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"password\"]} initialValue={initialValues.password} label={t(\"access.form.email_password.label\")} rules={[formRule]}>\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.email_password.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"senderAddress\"]}\n        initialValue={initialValues.senderAddress}\n        label={t(\"access.form.email_sender_address.label\")}\n        rules={[formRule]}\n      >\n        <Input type=\"email\" allowClear placeholder={t(\"access.form.email_sender_address.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"senderName\"]}\n        initialValue={initialValues.senderName}\n        label={t(\"access.form.email_sender_name.label\")}\n        rules={[formRule]}\n      >\n        <Input allowClear placeholder={t(\"access.form.email_sender_name.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"receiverAddress\"]}\n        initialValue={initialValues.receiverAddress}\n        label={t(\"access.form.email_receiver_address.label\")}\n        extra={t(\"access.form.email_receiver_address.help\")}\n        rules={[formRule]}\n      >\n        <Input type=\"email\" allowClear placeholder={t(\"access.form.email_receiver_address.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    smtpHost: \"\",\n    smtpPort: 465,\n    smtpTls: true,\n    username: \"\",\n    password: \"\",\n    senderAddress: \"\",\n    senderName: \"\",\n    receiverAddress: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    smtpHost: z.string().refine((v) => isHostname(v), t(\"common.errmsg.host_invalid\")),\n    smtpPort: z.coerce.number().refine((v) => isPortNumber(v), t(\"common.errmsg.port_invalid\")),\n    smtpTls: z.boolean().nullish(),\n    username: z.string().nonempty(t(\"access.form.email_username.placeholder\")),\n    password: z.string().nonempty(t(\"access.form.email_password.placeholder\")),\n    senderAddress: z.email(t(\"common.errmsg.email_invalid\")),\n    senderName: z.string().nullish(),\n    receiverAddress: z\n      .string()\n      .nullish()\n      .refine((v) => {\n        if (!v) return true;\n        return isEmail(v);\n      }, t(\"common.errmsg.email_invalid\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderEmail, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderFlexCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderFlexCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.flexcdn_server_url.label\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.flexcdn_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"apiRole\"]} initialValue={initialValues.apiRole} label={t(\"access.form.flexcdn_api_role.label\")} rules={[formRule]}>\n        <Radio.Group options={[\"user\", \"admin\"].map((s) => ({ label: t(`access.form.flexcdn_api_role.option.${s}.label`), value: s }))} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"accessKeyId\"]}\n        initialValue={initialValues.accessKeyId}\n        label={t(\"access.form.flexcdn_access_key_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.flexcdn_access_key_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.flexcdn_access_key_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"accessKey\"]}\n        initialValue={initialValues.accessKey}\n        label={t(\"access.form.flexcdn_access_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.flexcdn_access_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.flexcdn_access_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:8000/\",\n    apiRole: \"user\",\n    accessKeyId: \"\",\n    accessKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    apiRole: z.literal([\"user\", \"admin\"], t(\"access.form.flexcdn_api_role.placeholder\")),\n    accessKeyId: z.string().nonempty(t(\"access.form.flexcdn_access_key_id.placeholder\")),\n    accessKey: z.string().nonempty(t(\"access.form.flexcdn_access_key.placeholder\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderFlexCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderFlyIO.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderFlyIO = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiToken\"]}\n        initialValue={initialValues.apiToken}\n        label={t(\"access.form.flyio_api_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.flyio_api_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.flyio_api_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiToken: z.string().nonempty(t(\"access.form.flyio_api_token.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderFlyIO, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderGandinet.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderGandinet = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"personalAccessToken\"]}\n        initialValue={initialValues.personalAccessToken}\n        label={t(\"access.form.gandinet_personal_access_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.gandinet_personal_access_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.gandinet_personal_access_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    personalAccessToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    personalAccessToken: z.string().nonempty(t(\"access.form.gandinet_personal_access_token.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderGandinet, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderGcore.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderGcore = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiToken\"]}\n        initialValue={initialValues.apiToken}\n        label={t(\"access.form.gcore_api_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.gcore_api_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.gcore_api_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiToken: z.string().nonempty(t(\"access.form.gcore_api_token.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderGcore, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderGlobalSignAtlas.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderGobalSignAtlas = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"eabKid\"]} initialValue={initialValues.eabKid} label={t(\"access.form.shared_acme_eab_kid.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_kid.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"eabHmacKey\"]}\n        initialValue={initialValues.eabHmacKey}\n        label={t(\"access.form.shared_acme_eab_hmac_key.label\")}\n        rules={[formRule]}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_hmac_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.globalsignatlas_eab.guide\") }}></span>} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    eabKid: \"\",\n    eabHmacKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    eabKid: z.string().nonempty(t(\"access.form.shared_acme_eab_kid.placeholder\")),\n    eabHmacKey: z.string().nonempty(t(\"access.form.shared_acme_eab_hmac_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderGobalSignAtlas, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderGname.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderGname = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"appId\"]}\n        initialValue={initialValues.appId}\n        label={t(\"access.form.gname_app_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.gname_app_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.gname_app_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"appKey\"]}\n        initialValue={initialValues.appKey}\n        label={t(\"access.form.gname_app_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.gname_app_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.gname_app_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    appId: \"\",\n    appKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    appId: z.string().nonempty(t(\"access.form.gname_app_id.placeholder\")),\n    appKey: z.string().nonempty(t(\"access.form.gname_app_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderGname, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderGoDaddy.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderGoDaddy = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.godaddy_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.godaddy_api_key.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.godaddy_api_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiSecret\"]}\n        initialValue={initialValues.apiSecret}\n        label={t(\"access.form.godaddy_api_secret.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.godaddy_api_secret.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.godaddy_api_secret.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiKey: \"\",\n    apiSecret: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiKey: z.string().nonempty(t(\"access.form.godaddy_api_key.placeholder\")),\n    apiSecret: z.string().nonempty(t(\"access.form.godaddy_api_secret.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderGoDaddy, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderGoEdge.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderGoEdge = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.goedge_server_url.label\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.goedge_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"apiRole\"]} initialValue={initialValues.apiRole} label={t(\"access.form.goedge_api_role.label\")} rules={[formRule]}>\n        <Radio.Group options={[\"user\", \"admin\"].map((s) => ({ label: t(`access.form.goedge_api_role.option.${s}.label`), value: s }))} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"accessKeyId\"]}\n        initialValue={initialValues.accessKeyId}\n        label={t(\"access.form.goedge_access_key_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.goedge_access_key_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.goedge_access_key_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"accessKey\"]}\n        initialValue={initialValues.accessKey}\n        label={t(\"access.form.goedge_access_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.goedge_access_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.goedge_access_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:7788/\",\n    apiRole: \"user\",\n    accessKeyId: \"\",\n    accessKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    apiRole: z.literal([\"user\", \"admin\"], t(\"access.form.goedge_api_role.placeholder\")),\n    accessKeyId: z.string().nonempty(t(\"access.form.goedge_access_key_id.placeholder\")),\n    accessKey: z.string().nonempty(t(\"access.form.goedge_access_key.placeholder\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderGoEdge, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderGoogleTrustServices.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderGoogleTrustServices = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"eabKid\"]} initialValue={initialValues.eabKid} label={t(\"access.form.shared_acme_eab_kid.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_kid.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"eabHmacKey\"]}\n        initialValue={initialValues.eabHmacKey}\n        label={t(\"access.form.shared_acme_eab_hmac_key.label\")}\n        rules={[formRule]}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_hmac_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.googletrustservices_eab.guide\") }}></span>} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    eabKid: \"\",\n    eabHmacKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    eabKid: z.string().nonempty(t(\"access.form.shared_acme_eab_kid.placeholder\")),\n    eabHmacKey: z.string().nonempty(t(\"access.form.shared_acme_eab_hmac_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderGoogleTrustServices, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderHetzner.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderHetzner = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiToken\"]}\n        initialValue={initialValues.apiToken}\n        label={t(\"access.form.hetzner_api_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.hetzner_api_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.hetzner_api_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiToken: z.string().nonempty(t(\"access.form.hetzner_api_token.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderHetzner, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderHostingde.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderHostingde = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.hostingde_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.hostingde_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.hostingde_api_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiKey: z.string().nonempty(t(\"access.form.hostingde_api_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderHostingde, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderHostinger.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderHostinger = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiToken\"]}\n        initialValue={initialValues.apiToken}\n        label={t(\"access.form.hostinger_api_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.hostinger_api_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.hostinger_api_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiToken: z.string().nonempty(t(\"access.form.hostinger_api_token.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderHostinger, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderHuaweiCloud.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderHuaweiCloud = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessKeyId\"]}\n        initialValue={initialValues.accessKeyId}\n        label={t(\"access.form.huaweicloud_access_key_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.huaweicloud_access_key_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.huaweicloud_access_key_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretAccessKey\"]}\n        initialValue={initialValues.secretAccessKey}\n        label={t(\"access.form.huaweicloud_secret_access_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.huaweicloud_secret_access_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.huaweicloud_secret_access_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"enterpriseProjectId\"]}\n        initialValue={initialValues.enterpriseProjectId}\n        label={t(\"access.form.huaweicloud_enterprise_project_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.huaweicloud_enterprise_project_id.tooltip\") }}></span>}\n      >\n        <Input allowClear autoComplete=\"new-password\" placeholder={t(\"access.form.huaweicloud_enterprise_project_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessKeyId: \"\",\n    secretAccessKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessKeyId: z.string().nonempty(t(\"access.form.huaweicloud_access_key_id.placeholder\")),\n    secretAccessKey: z.string().nonempty(t(\"access.form.huaweicloud_secret_access_key.placeholder\")),\n    enterpriseProjectId: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderHuaweiCloud, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderIONOS.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderIONOS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiKeyPublicPrefix\"]}\n        initialValue={initialValues.apiKeyPublicPrefix}\n        label={t(\"access.form.ionos_api_key_public_prefix.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ionos_api_key_public_prefix.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.ionos_api_key_public_prefix.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiKeySecret\"]}\n        initialValue={initialValues.apiKeySecret}\n        label={t(\"access.form.ionos_api_key_secret.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ionos_api_key_secret.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.ionos_api_key_secret.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiKeyPublicPrefix: \"\",\n    apiKeySecret: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiKeyPublicPrefix: z.string().nonempty(t(\"access.form.ionos_api_key_public_prefix.placeholder\")),\n    apiKeySecret: z.string().nonempty(t(\"access.form.ionos_api_key_secret.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderIONOS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderInfomaniak.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderInfomaniak = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessToken\"]}\n        initialValue={initialValues.accessToken}\n        label={t(\"access.form.infomaniak_access_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.infomaniak_access_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.infomaniak_access_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessToken: z.string().nonempty(t(\"access.form.infomaniak_access_token.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderInfomaniak, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderJDCloud.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderJDCloud = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessKeyId\"]}\n        initialValue={initialValues.accessKeyId}\n        label={t(\"access.form.jdcloud_access_key_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.jdcloud_access_key_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.jdcloud_access_key_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"accessKeySecret\"]}\n        initialValue={initialValues.accessKeySecret}\n        label={t(\"access.form.jdcloud_access_key_secret.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.jdcloud_access_key_secret.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.jdcloud_access_key_secret.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessKeyId: \"\",\n    accessKeySecret: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessKeyId: z.string().nonempty(t(\"access.form.jdcloud_access_key_id.placeholder\")),\n    accessKeySecret: z.string().nonempty(t(\"access.form.jdcloud_access_key_secret.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderJDCloud, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderKong.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderKong = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"serverUrl\"]} initialValue={initialValues.serverUrl} label={t(\"access.form.kong_server_url.label\")} rules={[formRule]}>\n        <Input type=\"url\" placeholder={t(\"access.form.kong_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiToken\"]}\n        initialValue={initialValues.apiToken}\n        label={t(\"access.form.kong_api_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.kong_api_token.tooltip\") }}></span>}\n      >\n        <Input.Password allowClear autoComplete=\"new-password\" placeholder={t(\"access.form.kong_api_token.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:8001/\",\n    apiToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    apiToken: z.string().nullish(),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderKong, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderKsyun.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderKsyun = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessKeyId\"]}\n        initialValue={initialValues.accessKeyId}\n        label={t(\"access.form.ksyun_access_key_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ksyun_access_key_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.ksyun_access_key_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretAccessKey\"]}\n        initialValue={initialValues.secretAccessKey}\n        label={t(\"access.form.ksyun_secret_access_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ksyun_secret_access_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.ksyun_secret_access_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessKeyId: \"\",\n    secretAccessKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessKeyId: z.string().nonempty(t(\"access.form.ksyun_access_key_id.placeholder\")),\n    secretAccessKey: z.string().nonempty(t(\"access.form.ksyun_secret_access_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderKsyun, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderKubernetes.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport FileTextInput from \"@/components/FileTextInput\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderKubernetes = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"kubeConfig\"]}\n        initialValue={initialValues.kubeConfig}\n        label={t(\"access.form.k8s_kubeconfig.label\")}\n        extra={t(\"access.form.k8s_kubeconfig.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.k8s_kubeconfig.tooltip\") }}></span>}\n      >\n        <FileTextInput allowClear autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t(\"access.form.k8s_kubeconfig.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {};\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    kubeConfig: z\n      .string()\n      .max(20480, t(\"common.errmsg.string_max\", { max: 20480 }))\n      .nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderKubernetes, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderLarkBot.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Checkbox, Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport CodeTextInput from \"@/components/CodeTextInput\";\nimport { isJsonObject } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderLarkBot = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance<z.infer<typeof formSchema>>();\n  const initialValues = getInitialValues();\n\n  const fieldUseCustomPayload = Form.useWatch([parentNamePath, \"useCustomPayload\"], formInst);\n\n  const handleCustomPayloadChecked = (checked: boolean) => {\n    formInst.setFieldValue([parentNamePath, \"useCustomPayload\"], checked);\n    if (checked) {\n      formInst.setFieldValue([parentNamePath, \"customPayload\"], commonPayloadString);\n    } else {\n      formInst.setFieldValue([parentNamePath, \"customPayload\"], void 0);\n    }\n  };\n\n  const handleCustomPayloadBlur = () => {\n    const value = formInst.getFieldValue([parentNamePath, \"customPayload\"]);\n    try {\n      const json = JSON.stringify(JSON.parse(value), null, 2);\n      formInst.setFieldValue([parentNamePath, \"customPayload\"], json);\n    } catch {\n      return;\n    }\n  };\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"webhookUrl\"]}\n        initialValue={initialValues.webhookUrl}\n        label={t(\"access.form.larkbot_webhook_url.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.larkbot_webhook_url.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"access.form.larkbot_webhook_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secret\"]}\n        initialValue={initialValues.secret}\n        label={t(\"access.form.larkbot_secret.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.larkbot_secret.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.larkbot_secret.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item label={t(\"access.form.larkbot_custom_payload.label\")}>\n        <Form.Item name={[parentNamePath, \"useCustomPayload\"]} noStyle>\n          <Checkbox checked={!!fieldUseCustomPayload} onChange={(e) => handleCustomPayloadChecked(e.target.checked)}>\n            {t(\"access.form.larkbot_custom_payload.checkbox\")}\n          </Checkbox>\n        </Form.Item>\n        <Form.Item\n          name={[parentNamePath, \"customPayload\"]}\n          hidden={!fieldUseCustomPayload}\n          initialValue={initialValues.customPayload}\n          noStyle\n          rules={[formRule]}\n        >\n          <CodeTextInput\n            className=\"mt-2\"\n            lineWrapping={false}\n            height=\"auto\"\n            minHeight=\"64px\"\n            maxHeight=\"256px\"\n            language=\"json\"\n            placeholder={t(\"access.form.larkbot_custom_payload.placeholder\")}\n            onBlur={handleCustomPayloadBlur}\n          />\n        </Form.Item>\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    webhookUrl: \"\",\n    secret: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      webhookUrl: z.url(t(\"common.errmsg.url_invalid\")),\n      secret: z.string().nullish(),\n      useCustomPayload: z.boolean().nullish(),\n      customPayload: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.useCustomPayload) {\n        if (!isJsonObject(values.customPayload!)) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: t(\"common.errmsg.json_invalid\"),\n            path: [\"customPayload\"],\n          });\n        }\n      }\n    });\n};\n\nconst commonPayloadString = JSON.stringify(\n  {\n    msg_type: \"text\",\n    content: {\n      text: \"${CERTIMATE_NOTIFIER_SUBJECT}\\n\\n${CERTIMATE_NOTIFIER_MESSAGE}\",\n    },\n  },\n  null,\n  2\n);\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderLarkBot, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderLeCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio, Select, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderLeCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"serverUrl\"]} initialValue={initialValues.serverUrl} label={t(\"access.form.lecdn_server_url.label\")} rules={[formRule]}>\n        <Input type=\"url\" placeholder={t(\"access.form.lecdn_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiVersion\"]}\n        initialValue={initialValues.apiVersion}\n        label={t(\"access.form.lecdn_api_version.label\")}\n        rules={[formRule]}\n      >\n        <Select options={[\"v3\"].map((s) => ({ label: s, value: s }))} placeholder={t(\"access.form.lecdn_api_version.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"apiRole\"]} initialValue={initialValues.apiRole} label={t(\"access.form.lecdn_api_role.label\")} rules={[formRule]}>\n        <Radio.Group options={[\"user\", \"master\"].map((s) => ({ label: t(`access.form.lecdn_api_role.option.${s}.label`), value: s }))} />\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"username\"]} initialValue={initialValues.username} label={t(\"access.form.lecdn_username.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.lecdn_username.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"password\"]} initialValue={initialValues.password} label={t(\"access.form.lecdn_password.label\")} rules={[formRule]}>\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.lecdn_password.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:5090/\",\n    apiVersion: \"v3\",\n    apiRole: \"client\",\n    username: \"\",\n    password: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    apiVersion: z.literal([\"v3\"], t(\"access.form.lecdn_api_version.placeholder\")),\n    apiRole: z.literal([\"client\", \"master\"], t(\"access.form.lecdn_api_role.placeholder\")),\n    username: z.string().nonempty(t(\"access.form.lecdn_username.placeholder\")),\n    password: z.string().nonempty(t(\"access.form.lecdn_password.placeholder\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderLeCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderLinode.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderLinode = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessToken\"]}\n        initialValue={initialValues.accessToken}\n        label={t(\"access.form.linode_access_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.linode_access_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.linode_access_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessToken: z.string().nonempty(t(\"access.form.linode_access_token.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderLinode, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderLiteSSL.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderLiteSSL = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"eabKid\"]} initialValue={initialValues.eabKid} label={t(\"access.form.shared_acme_eab_kid.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_kid.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"eabHmacKey\"]}\n        initialValue={initialValues.eabHmacKey}\n        label={t(\"access.form.shared_acme_eab_hmac_key.label\")}\n        rules={[formRule]}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_hmac_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.litessl_eab.guide\") }}></span>} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    eabKid: \"\",\n    eabHmacKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    eabKid: z.string().nonempty(t(\"access.form.shared_acme_eab_kid.placeholder\")),\n    eabHmacKey: z.string().nonempty(t(\"access.form.shared_acme_eab_hmac_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderLiteSSL, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderMattermost.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderMattermost = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.mattermost_server_url.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.mattermost_server_url.tooltip\") }}></span>}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.mattermost_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"username\"]}\n        initialValue={initialValues.username}\n        label={t(\"access.form.mattermost_username.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"access.form.mattermost_username.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"password\"]}\n        initialValue={initialValues.password}\n        label={t(\"access.form.mattermost_password.label\")}\n        rules={[formRule]}\n      >\n        <Input.Password placeholder={t(\"access.form.mattermost_password.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"channelId\"]}\n        initialValue={initialValues.channelId}\n        label={t(\"access.form.mattermost_channel_id.label\")}\n        extra={t(\"access.form.mattermost_channel_id.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.mattermost_channel_id.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"access.form.mattermost_channel_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:8065/\",\n    username: \"\",\n    password: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    username: z.string().nonempty(t(\"access.form.mattermost_username.placeholder\")),\n    password: z.string().nonempty(t(\"access.form.mattermost_password.placeholder\")),\n    channelId: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderMattermost, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderMohua.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\r\nimport { Form, Input } from \"antd\";\r\nimport { createSchemaFieldRule } from \"antd-zod\";\r\nimport { z } from \"zod\";\r\n\r\nimport { useFormNestedFieldsContext } from \"./_context\";\r\n\r\nconst AccessConfigFormFieldsProviderMohua = () => {\r\n  const { i18n, t } = useTranslation();\r\n\r\n  const { parentNamePath } = useFormNestedFieldsContext();\r\n  const formSchema = z.object({\r\n    [parentNamePath]: getSchema({ i18n }),\r\n  });\r\n  const formRule = createSchemaFieldRule(formSchema);\r\n  const initialValues = getInitialValues();\r\n\r\n  return (\r\n    <>\r\n      <Form.Item name={[parentNamePath, \"username\"]} initialValue={initialValues.username} label={t(\"access.form.mohua_username.label\")} rules={[formRule]}>\r\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.mohua_username.placeholder\")} />\r\n      </Form.Item>\r\n\r\n      <Form.Item\r\n        name={[parentNamePath, \"apiPassword\"]}\r\n        initialValue={initialValues.apiPassword}\r\n        label={t(\"access.form.mohua_api_password.label\")}\r\n        rules={[formRule]}\r\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.mohua_api_password.tooltip\") }}></span>}\r\n      >\r\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.mohua_api_password.placeholder\")} />\r\n      </Form.Item>\r\n    </>\r\n  );\r\n};\r\n\r\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\r\n  return {\r\n    username: \"\",\r\n    apiPassword: \"\",\r\n  };\r\n};\r\n\r\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\r\n  const { t } = i18n;\r\n\r\n  return z.object({\r\n    username: z.string().nonempty(t(\"access.form.mohua_username.placeholder\")),\r\n    apiPassword: z.string().nonempty(t(\"access.form.mohua_api_password.placeholder\")),\r\n  });\r\n};\r\n\r\nconst _default = Object.assign(AccessConfigFormFieldsProviderMohua, {\r\n  getInitialValues,\r\n  getSchema,\r\n});\r\n\r\nexport default _default;\r\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderNS1.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderNS1 = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.ns1_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ns1_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.ns1_api_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiKey: z.string().nonempty(t(\"access.form.ns1_api_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderNS1, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderNameDotCom.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderNameDotCom = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"username\"]}\n        initialValue={initialValues.username}\n        label={t(\"access.form.namedotcom_username.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.namedotcom_username.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.namedotcom_username.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiToken\"]}\n        initialValue={initialValues.apiToken}\n        label={t(\"access.form.namedotcom_api_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.namedotcom_api_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.namedotcom_api_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    username: \"\",\n    apiToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    username: z.string().nonempty(t(\"access.form.namedotcom_username.placeholder\")),\n    apiToken: z.string().nonempty(t(\"access.form.namedotcom_api_token.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderNameDotCom, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderNameSilo.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderNameSilo = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.namesilo_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.namesilo_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.namesilo_api_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiKey: z.string().nonempty(t(\"access.form.namesilo_api_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderNameSilo, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderNamecheap.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderNamecheap = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"username\"]}\n        initialValue={initialValues.username}\n        label={t(\"access.form.namecheap_username.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.namecheap_username.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.namecheap_username.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.namecheap_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.namecheap_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.namecheap_api_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    username: \"\",\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    username: z.string().nonempty(t(\"access.form.namecheap_username.placeholder\")),\n    apiKey: z.string().nonempty(t(\"access.form.namecheap_api_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderNamecheap, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderNetcup.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderNetcup = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"customerNumber\"]}\n        initialValue={initialValues.customerNumber}\n        label={t(\"access.form.netcup_customer_number.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.netcup_customer_number.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.netcup_customer_number.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.netcup_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.netcup_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.netcup_api_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiPassword\"]}\n        initialValue={initialValues.apiPassword}\n        label={t(\"access.form.netcup_api_password.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.netcup_api_password.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.netcup_api_password.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    customerNumber: \"\",\n    apiKey: \"\",\n    apiPassword: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    customerNumber: z.string().nonempty(t(\"access.form.netcup_customer_number.placeholder\")),\n    apiKey: z.string().nonempty(t(\"access.form.netcup_api_key.placeholder\")),\n    apiPassword: z.string().nonempty(t(\"access.form.netcup_api_password.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderNetcup, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderNetlify.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderNetlify = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiToken\"]}\n        initialValue={initialValues.apiToken}\n        label={t(\"access.form.netlify_api_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.netlify_api_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.netlify_api_token.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiToken: z.string().nonempty(t(\"access.form.netlify_api_token.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderNetlify, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderNginxProxyManager.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AUTH_METHOD_PASSWORD = \"password\" as const;\nconst AUTH_METHOD_TOKEN = \"token\" as const;\n\nconst AccessConfigFormFieldsProviderNginxProxyManager = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldAuthMethod = Form.useWatch<string>([parentNamePath, \"authMethod\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.nginxproxymanager_server_url.label\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.nginxproxymanager_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"authMethod\"]}\n        initialValue={initialValues.authMethod}\n        label={t(\"access.form.nginxproxymanager_auth_method.label\")}\n        rules={[formRule]}\n      >\n        <Radio.Group block>\n          <Radio.Button value={AUTH_METHOD_PASSWORD}>{t(\"access.form.nginxproxymanager_auth_method.option.password.label\")}</Radio.Button>\n          <Radio.Button value={AUTH_METHOD_TOKEN}>{t(\"access.form.nginxproxymanager_auth_method.option.token.label\")}</Radio.Button>\n        </Radio.Group>\n      </Form.Item>\n\n      <Show when={fieldAuthMethod === AUTH_METHOD_PASSWORD}>\n        <Form.Item\n          name={[parentNamePath, \"username\"]}\n          initialValue={initialValues.username}\n          label={t(\"access.form.nginxproxymanager_username.label\")}\n          rules={[formRule]}\n        >\n          <Input autoComplete=\"new-password\" placeholder={t(\"access.form.nginxproxymanager_username.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"password\"]}\n          initialValue={initialValues.password}\n          label={t(\"access.form.nginxproxymanager_password.label\")}\n          rules={[formRule]}\n        >\n          <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.nginxproxymanager_password.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldAuthMethod === AUTH_METHOD_TOKEN}>\n        <Form.Item\n          name={[parentNamePath, \"apiToken\"]}\n          initialValue={initialValues.apiToken}\n          label={t(\"access.form.nginxproxymanager_api_token.label\")}\n          rules={[formRule]}\n        >\n          <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.nginxproxymanager_api_token.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:81/\",\n    authMethod: AUTH_METHOD_PASSWORD,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n      authMethod: z.literal([AUTH_METHOD_PASSWORD, AUTH_METHOD_TOKEN], t(\"access.form.nginxproxymanager_auth_method.placeholder\")),\n      username: z.string().nullish(),\n      password: z.string().nullish(),\n      apiToken: z.string().nullish(),\n      allowInsecureConnections: z.boolean().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.authMethod) {\n        case AUTH_METHOD_PASSWORD:\n          {\n            if (!values.username?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"access.form.nginxproxymanager_username.placeholder\"),\n                path: [\"username\"],\n              });\n            }\n\n            if (!values.password?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"access.form.nginxproxymanager_password.placeholder\"),\n                path: [\"password\"],\n              });\n            }\n          }\n          break;\n\n        case AUTH_METHOD_TOKEN:\n          {\n            if (!values.apiToken?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"access.form.nginxproxymanager_api_token.placeholder\"),\n                path: [\"apiToken\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderNginxProxyManager, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderOVHcloud.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { AutoComplete, Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { matchSearchOption } from \"@/utils/search\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AUTH_METHOD_APPLICATION = \"application\" as const;\nconst AUTH_METHOD_OAUTH2 = \"oauth2\" as const;\n\nconst AccessConfigFormFieldsProviderOVHcloud = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldAuthMethod = Form.useWatch<string>([parentNamePath, \"authMethod\"], formInst);\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"endpoint\"]} initialValue={initialValues.endpoint} label={t(\"access.form.ovhcloud_endpoint.label\")} rules={[formRule]}>\n        <AutoComplete\n          options={[\"ovh-eu\", \"ovh-us\", \"ovh-ca\"].map((value) => ({ value }))}\n          placeholder={t(\"access.form.ovhcloud_endpoint.placeholder\")}\n          showSearch={{\n            filterOption: (inputValue, option) => matchSearchOption(inputValue, option!),\n          }}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"authMethod\"]}\n        initialValue={initialValues.authMethod}\n        label={t(\"access.form.ovhcloud_auth_method.label\")}\n        rules={[formRule]}\n      >\n        <Radio.Group block>\n          <Radio.Button value={AUTH_METHOD_APPLICATION}>{t(\"access.form.ovhcloud_auth_method.option.application.label\")}</Radio.Button>\n          <Radio.Button value={AUTH_METHOD_OAUTH2}>{t(\"access.form.ovhcloud_auth_method.option.oauth2.label\")}</Radio.Button>\n        </Radio.Group>\n      </Form.Item>\n\n      <Show when={fieldAuthMethod === AUTH_METHOD_APPLICATION}>\n        <Form.Item\n          name={[parentNamePath, \"applicationKey\"]}\n          initialValue={initialValues.applicationKey}\n          label={t(\"access.form.ovhcloud_application_key.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ovhcloud_application_key.tooltip\") }}></span>}\n        >\n          <Input autoComplete=\"new-password\" placeholder={t(\"access.form.ovhcloud_application_key.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"applicationSecret\"]}\n          initialValue={initialValues.applicationSecret}\n          label={t(\"access.form.ovhcloud_application_secret.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ovhcloud_application_secret.tooltip\") }}></span>}\n        >\n          <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.ovhcloud_application_secret.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"consumerKey\"]}\n          initialValue={initialValues.consumerKey}\n          label={t(\"access.form.ovhcloud_consumer_key.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ovhcloud_consumer_key.tooltip\") }}></span>}\n        >\n          <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.ovhcloud_consumer_key.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldAuthMethod === AUTH_METHOD_OAUTH2}>\n        <Form.Item\n          name={[parentNamePath, \"clientId\"]}\n          initialValue={initialValues.clientId}\n          label={t(\"access.form.ovhcloud_client_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ovhcloud_client_id.tooltip\") }}></span>}\n        >\n          <Input autoComplete=\"new-password\" placeholder={t(\"access.form.ovhcloud_client_id.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"clientSecret\"]}\n          initialValue={initialValues.clientSecret}\n          label={t(\"access.form.ovhcloud_client_secret.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ovhcloud_client_secret.tooltip\") }}></span>}\n        >\n          <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.ovhcloud_client_secret.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    endpoint: \"ovh-eu\",\n    authMethod: AUTH_METHOD_APPLICATION,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      endpoint: z.string().nonempty(t(\"access.form.ovhcloud_endpoint.placeholder\")),\n      authMethod: z.literal([AUTH_METHOD_APPLICATION, AUTH_METHOD_OAUTH2], t(\"access.form.ovhcloud_auth_method.placeholder\")),\n      applicationKey: z.string().nullish(),\n      applicationSecret: z.string().nullish(),\n      consumerKey: z.string().nullish(),\n      clientId: z.string().nullish(),\n      clientSecret: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.authMethod) {\n        case AUTH_METHOD_APPLICATION:\n          {\n            if (!values.applicationKey?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"access.form.ovhcloud_application_key.placeholder\"),\n                path: [\"applicationKey\"],\n              });\n            }\n\n            if (!values.applicationSecret?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"access.form.ovhcloud_application_secret.placeholder\"),\n                path: [\"applicationSecret\"],\n              });\n            }\n\n            if (!values.consumerKey?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"access.form.ovhcloud_consumer_key.placeholder\"),\n                path: [\"consumerKey\"],\n              });\n            }\n          }\n          break;\n\n        case AUTH_METHOD_OAUTH2:\n          {\n            if (!values.clientId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"access.form.ovhcloud_client_id.placeholder\"),\n                path: [\"clientId\"],\n              });\n            }\n\n            if (!values.clientSecret?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"access.form.ovhcloud_client_secret.placeholder\"),\n                path: [\"clientSecret\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderOVHcloud, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderPorkbun.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderPorkbun = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.porkbun_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.porkbun_api_key.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.porkbun_api_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretApiKey\"]}\n        initialValue={initialValues.secretApiKey}\n        label={t(\"access.form.porkbun_secret_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.porkbun_secret_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.porkbun_secret_api_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiKey: \"\",\n    secretApiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiKey: z.string().nonempty(t(\"access.form.porkbun_api_key.placeholder\")),\n    secretApiKey: z.string().nonempty(t(\"access.form.porkbun_secret_api_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderPorkbun, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderPowerDNS.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderPowerDNS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.powerdns_server_url.label\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.powerdns_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.powerdns_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.powerdns_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.powerdns_api_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:8082/\",\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    apiKey: z.string().nonempty(t(\"access.form.powerdns_api_key.placeholder\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderPowerDNS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderProxmoxVE.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderProxmoxVE = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.proxmoxve_server_url.label\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.proxmoxve_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiToken\"]}\n        initialValue={initialValues.apiToken}\n        label={t(\"access.form.proxmoxve_api_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.proxmoxve_api_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.proxmoxve_api_token.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiTokenSecret\"]}\n        initialValue={initialValues.apiTokenSecret}\n        label={t(\"access.form.proxmoxve_api_token_secret.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.proxmoxve_api_token_secret.tooltip\") }}></span>}\n      >\n        <Input.Password allowClear autoComplete=\"new-password\" placeholder={t(\"access.form.proxmoxve_api_token_secret.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:8006/\",\n    apiToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    apiToken: z.string().nonempty(t(\"access.form.proxmoxve_api_token.placeholder\")),\n    apiTokenSecret: z.string().nullish(),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderProxmoxVE, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderQingCloud.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderQingCloud = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessKeyId\"]}\n        initialValue={initialValues.accessKeyId}\n        label={t(\"access.form.qingcloud_access_key_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.qingcloud_access_key_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.qingcloud_access_key_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretAccessKey\"]}\n        initialValue={initialValues.secretAccessKey}\n        label={t(\"access.form.qingcloud_secret_access_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.qingcloud_secret_access_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.qingcloud_secret_access_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessKeyId: \"\",\n    secretAccessKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessKeyId: z.string().nonempty(t(\"access.form.qingcloud_access_key_id.placeholder\")),\n    secretAccessKey: z.string().nonempty(t(\"access.form.qingcloud_secret_access_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderQingCloud, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderQiniu.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderQiniu = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessKey\"]}\n        initialValue={initialValues.accessKey}\n        label={t(\"access.form.qiniu_access_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.qiniu_access_key.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.qiniu_access_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretKey\"]}\n        initialValue={initialValues.secretKey}\n        label={t(\"access.form.qiniu_secret_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.qiniu_secret_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.qiniu_secret_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessKey: \"\",\n    secretKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessKey: z.string().nonempty(t(\"access.form.qiniu_access_key.placeholder\")),\n    secretKey: z.string().nonempty(t(\"access.form.qiniu_secret_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderQiniu, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderRFC2136.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, InputNumber, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isHostname, isPortNumber } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderRFC2136 = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <div className=\"flex space-x-2\">\n        <div className=\"w-2/3\">\n          <Form.Item name={[parentNamePath, \"host\"]} initialValue={initialValues.host} label={t(\"access.form.rfc2136_host.label\")} rules={[formRule]}>\n            <Input placeholder={t(\"access.form.rfc2136_host.placeholder\")} />\n          </Form.Item>\n        </div>\n\n        <div className=\"w-1/3\">\n          <Form.Item name={[parentNamePath, \"port\"]} initialValue={initialValues.port} label={t(\"access.form.rfc2136_port.label\")} rules={[formRule]}>\n            <InputNumber style={{ width: \"100%\" }} min={1} max={65535} placeholder={t(\"access.form.rfc2136_port.placeholder\")} />\n          </Form.Item>\n        </div>\n      </div>\n\n      <Form.Item\n        name={[parentNamePath, \"tsigAlgorithm\"]}\n        initialValue={initialValues.tsigAlgorithm}\n        label={t(\"access.form.rfc2136_tsig_algorithm.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[\n            { label: \"HMAC-SHA-1\", value: \"hmac-sha1.\" },\n            { label: \"HMAC-SHA-224\", value: \"hmac-sha224.\" },\n            { label: \"HMAC-SHA-256\", value: \"hmac-sha256.\" },\n            { label: \"HMAC-SHA-384\", value: \"hmac-sha384.\" },\n            { label: \"HMAC-SHA-512\", value: \"hmac-sha512.\" },\n          ]}\n          placeholder={t(\"access.form.rfc2136_tsig_algorithm.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"tsigKey\"]} initialValue={initialValues.tsigKey} label={t(\"access.form.rfc2136_tsig_key.label\")} rules={[formRule]}>\n        <Input allowClear autoComplete=\"new-password\" placeholder={t(\"access.form.rfc2136_tsig_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"tsigSecret\"]}\n        initialValue={initialValues.tsigSecret}\n        label={t(\"access.form.rfc2136_tsig_secret.label\")}\n        rules={[formRule]}\n      >\n        <Input.Password allowClear autoComplete=\"new-password\" placeholder={t(\"access.form.rfc2136_tsig_secret.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    host: \"127.0.0.1\",\n    port: 53,\n    tsigAlgorithm: \"hmac-sha1.\",\n    tsigKey: \"\",\n    tsigSecret: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    host: z.string().refine((v) => isHostname(v), t(\"common.errmsg.host_invalid\")),\n    port: z.coerce.number().refine((v) => isPortNumber(v), t(\"common.errmsg.port_invalid\")),\n    tsigAlgorithm: z.string().nonempty(t(\"access.form.rfc2136_tsig_algorithm.placeholder\")),\n    tsigKey: z.string().nullish(),\n    tsigSecret: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderRFC2136, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderRainYun.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderRainYun = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.rainyun_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.rainyun_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.rainyun_api_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiKey: z.string().nonempty(t(\"access.form.rainyun_api_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderRainYun, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderRatPanel.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderRatPanel = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.ratpanel_server_url.label\")}\n        extra={t(\"access.form.ratpanel_server_url.help\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.ratpanel_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"accessTokenId\"]}\n        initialValue={initialValues.accessTokenId}\n        label={t(\"access.form.ratpanel_access_token_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ratpanel_access_token_id.tooltip\") }}></span>}\n      >\n        <Input type=\"number\" placeholder={t(\"access.form.ratpanel_access_token_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"accessToken\"]}\n        initialValue={initialValues.accessToken}\n        label={t(\"access.form.ratpanel_access_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ratpanel_access_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.ratpanel_access_token.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:8888/\",\n    accessTokenId: 1,\n    accessToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    accessTokenId: z.coerce.number().int().positive(),\n    accessToken: z.string().nonempty(t(\"access.form.ratpanel_access_token.placeholder\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderRatPanel, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderS3.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isHostname, isUrlWithHttpOrHttps } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFieldsProviderS3 = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"endpoint\"]}\n        initialValue={initialValues.endpoint}\n        label={t(\"access.form.s3_endpoint.label\")}\n        extra={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.s3_endpoint.help\") }}></span>}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"access.form.s3_endpoint.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"accessKey\"]} initialValue={initialValues.accessKey} label={t(\"access.form.s3_access_key.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.s3_access_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"secretKey\"]} initialValue={initialValues.secretKey} label={t(\"access.form.s3_secret_key.label\")} rules={[formRule]}>\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.s3_secret_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"signatureVersion\"]}\n        initialValue={initialValues.signatureVersion}\n        label={t(\"access.form.s3_signature_version.label\")}\n        rules={[formRule]}\n      >\n        <Select options={[\"v2\", \"v4\"].map((s) => ({ label: s, value: s }))} placeholder={t(\"access.form.s3_signature_version.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"usePathStyle\"]}\n        initialValue={initialValues.usePathStyle}\n        label={t(\"access.form.s3_use_path_style.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.s3_use_path_style.tooltip\") }}></span>}\n      >\n        <Switch />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    endpoint: \"\",\n    accessKey: \"\",\n    secretKey: \"\",\n    signatureVersion: \"v4\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    endpoint: z.string().refine((v) => isHostname(v) || isUrlWithHttpOrHttps(v), t(\"access.form.s3_endpoint.placeholder\")),\n    accessKey: z.string().nonempty(t(\"access.form.s3_access_key.placeholder\")),\n    secretKey: z.string().nonempty(t(\"access.form.s3_secret_key.placeholder\")),\n    signatureVersion: z.enum([\"v2\", \"v4\"]),\n    usePathStyle: z.boolean().nullish(),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFieldsProviderS3, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderSSH.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { IconCircleArrowDown, IconCircleArrowUp, IconCircleMinus, IconCirclePlus } from \"@tabler/icons-react\";\nimport { Button, Collapse, Form, Input, InputNumber, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport FileTextInput from \"@/components/FileTextInput\";\nimport Show from \"@/components/Show\";\nimport { mergeCls } from \"@/utils/css\";\nimport { isHostname, isPortNumber } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AUTH_METHOD_NONE = \"none\" as const;\nconst AUTH_METHOD_PASSWORD = \"password\" as const;\nconst AUTH_METHOD_KEY = \"key\" as const;\n\nconst AccessConfigFormFieldsProviderSSH = ({ disabled }: { disabled?: boolean }) => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldAuthMethod = Form.useWatch<string>([parentNamePath, \"authMethod\"], formInst);\n  const fieldJumpServers = Form.useWatch<any[]>([parentNamePath, \"jumpServers\"], formInst);\n\n  return (\n    <>\n      <div className=\"flex space-x-2\">\n        <div className=\"w-2/3\">\n          <Form.Item name={[parentNamePath, \"host\"]} initialValue={initialValues.host} label={t(\"access.form.ssh_host.label\")} rules={[formRule]}>\n            <Input placeholder={t(\"access.form.ssh_host.placeholder\")} />\n          </Form.Item>\n        </div>\n\n        <div className=\"w-1/3\">\n          <Form.Item name={[parentNamePath, \"port\"]} initialValue={initialValues.port} label={t(\"access.form.ssh_port.label\")} rules={[formRule]}>\n            <InputNumber style={{ width: \"100%\" }} min={1} max={65535} placeholder={t(\"access.form.ssh_port.placeholder\")} />\n          </Form.Item>\n        </div>\n      </div>\n\n      <Form.Item\n        name={[parentNamePath, \"authMethod\"]}\n        initialValue={initialValues.authMethod}\n        label={t(\"access.form.ssh_auth_method.label\")}\n        rules={[formRule]}\n      >\n        <Radio.Group block>\n          <Radio.Button value={AUTH_METHOD_NONE}>{t(\"access.form.ssh_auth_method.option.none.label\")}</Radio.Button>\n          <Radio.Button value={AUTH_METHOD_PASSWORD}>{t(\"access.form.ssh_auth_method.option.password.label\")}</Radio.Button>\n          <Radio.Button value={AUTH_METHOD_KEY}>{t(\"access.form.ssh_auth_method.option.key.label\")}</Radio.Button>\n        </Radio.Group>\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"username\"]} initialValue={initialValues.username} label={t(\"access.form.ssh_username.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.ssh_username.placeholder\")} />\n      </Form.Item>\n\n      <Show when={fieldAuthMethod === AUTH_METHOD_PASSWORD}>\n        <Form.Item name={[parentNamePath, \"password\"]} initialValue={initialValues.password} label={t(\"access.form.ssh_password.label\")} rules={[formRule]}>\n          <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.ssh_password.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldAuthMethod === AUTH_METHOD_KEY}>\n        <Form.Item name={[parentNamePath, \"key\"]} initialValue={initialValues.key} label={t(\"access.form.ssh_key.label\")} rules={[formRule]}>\n          <FileTextInput autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t(\"access.form.ssh_key.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"keyPassphrase\"]}\n          initialValue={initialValues.keyPassphrase}\n          label={t(\"access.form.ssh_key_passphrase.label\")}\n          rules={[formRule]}\n        >\n          <Input.Password allowClear autoComplete=\"new-password\" placeholder={t(\"access.form.ssh_key_passphrase.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Form.Item label={t(\"access.form.ssh_jump_servers.label\")}>\n        <Form.List name={[parentNamePath, \"jumpServers\"]}>\n          {(fields, { add, remove, move }) => (\n            <div className=\"flex flex-col gap-2\">\n              <Collapse\n                className={mergeCls({ hidden: !fields.length })}\n                items={fields?.map(({ key, name: index }) => {\n                  const subfieldHost = fieldJumpServers?.[index]?.host;\n                  const subfieldPort = fieldJumpServers?.[index]?.post;\n                  const subfieldAuthMethod = fieldJumpServers?.[index]?.authMethod;\n\n                  const subfieldHostAndPort =\n                    !!subfieldHost && !!subfieldPort\n                      ? `${subfieldHost}:${subfieldPort}`\n                      : subfieldHost\n                        ? subfieldHost\n                        : subfieldPort\n                          ? `:${subfieldPort}`\n                          : \"unknown\";\n\n                  return {\n                    key: key,\n                    forceRender: true,\n                    label: (\n                      <span className=\"select-none\">\n                        [{t(\"access.form.ssh_jump_servers.item.label\")} {index + 1}] {subfieldHostAndPort}\n                      </span>\n                    ),\n                    extra: !disabled && (\n                      <div className=\"flex items-center justify-end\">\n                        <Button\n                          icon={<IconCircleArrowUp size=\"1.25em\" />}\n                          color=\"default\"\n                          disabled={index === 0}\n                          size=\"small\"\n                          type=\"text\"\n                          onClick={(e) => {\n                            move(index, index - 1);\n                            e.stopPropagation();\n                          }}\n                        />\n                        <Button\n                          icon={<IconCircleArrowDown size=\"1.25em\" />}\n                          color=\"default\"\n                          disabled={index === fields.length - 1}\n                          size=\"small\"\n                          type=\"text\"\n                          onClick={(e) => {\n                            move(index, index + 1);\n                            e.stopPropagation();\n                          }}\n                        />\n                        <Button\n                          icon={<IconCircleMinus size=\"1.25em\" />}\n                          color=\"default\"\n                          size=\"small\"\n                          type=\"text\"\n                          onClick={(e) => {\n                            remove(index);\n                            e.stopPropagation();\n                          }}\n                        />\n                      </div>\n                    ),\n                    children: (\n                      <>\n                        <div className=\"flex space-x-2\">\n                          <div className=\"w-2/3\">\n                            <Form.Item name={[index, \"host\"]} label={t(\"access.form.ssh_host.label\")} shouldUpdate rules={[formRule]}>\n                              <Input placeholder={t(\"access.form.ssh_host.placeholder\")} />\n                            </Form.Item>\n                          </div>\n                          <div className=\"w-1/3\">\n                            <Form.Item name={[index, \"port\"]} label={t(\"access.form.ssh_port.label\")} shouldUpdate rules={[formRule]}>\n                              <InputNumber style={{ width: \"100%\" }} placeholder={t(\"access.form.ssh_port.placeholder\")} min={1} max={65535} />\n                            </Form.Item>\n                          </div>\n                        </div>\n\n                        <Form.Item name={[index, \"authMethod\"]} label={t(\"access.form.ssh_auth_method.label\")} shouldUpdate rules={[formRule]}>\n                          <Radio.Group\n                            options={[AUTH_METHOD_NONE, AUTH_METHOD_PASSWORD, AUTH_METHOD_KEY].map((s) => ({\n                              key: s,\n                              label: t(`access.form.ssh_auth_method.option.${s}.label`),\n                              value: s,\n                            }))}\n                          />\n                        </Form.Item>\n\n                        <Form.Item name={[index, \"username\"]} label={t(\"access.form.ssh_username.label\")} shouldUpdate rules={[formRule]}>\n                          <Input autoComplete=\"new-password\" placeholder={t(\"access.form.ssh_username.placeholder\")} />\n                        </Form.Item>\n\n                        <Form.Item\n                          name={[index, \"password\"]}\n                          hidden={subfieldAuthMethod !== AUTH_METHOD_PASSWORD}\n                          label={t(\"access.form.ssh_password.label\")}\n                          shouldUpdate\n                          rules={[formRule]}\n                        >\n                          <Input.Password allowClear autoComplete=\"new-password\" placeholder={t(\"access.form.ssh_password.placeholder\")} />\n                        </Form.Item>\n\n                        <Form.Item\n                          name={[index, \"key\"]}\n                          hidden={subfieldAuthMethod !== AUTH_METHOD_KEY}\n                          label={t(\"access.form.ssh_key.label\")}\n                          shouldUpdate\n                          rules={[formRule]}\n                        >\n                          <FileTextInput allowClear autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t(\"access.form.ssh_key.placeholder\")} />\n                        </Form.Item>\n\n                        <Form.Item\n                          name={[index, \"keyPassphrase\"]}\n                          hidden={subfieldAuthMethod !== AUTH_METHOD_KEY}\n                          label={t(\"access.form.ssh_key_passphrase.label\")}\n                          shouldUpdate\n                          rules={[formRule]}\n                        >\n                          <Input.Password allowClear autoComplete=\"new-password\" placeholder={t(\"access.form.ssh_key_passphrase.placeholder\")} />\n                        </Form.Item>\n                      </>\n                    ),\n                  };\n                })}\n              />\n              <Button\n                className=\"w-full\"\n                type=\"dashed\"\n                icon={<IconCirclePlus size=\"1.25em\" />}\n                onClick={() => {\n                  add();\n                  setTimeout(() => {\n                    const jumpServer = getInitialValues();\n                    delete jumpServer.jumpServers;\n                    formInst.setFieldValue([parentNamePath, \"jumpServers\", (fieldJumpServers?.length ?? 0) + 1 - 1], jumpServer);\n                  }, 0);\n                }}\n              >\n                {t(\"access.form.ssh_jump_servers.add.button\")}\n              </Button>\n            </div>\n          )}\n        </Form.List>\n        <Form.Item name={[parentNamePath, \"jumpServers\"]} noStyle rules={[formRule]} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    host: \"127.0.0.1\",\n    port: 22,\n    authMethod: AUTH_METHOD_PASSWORD,\n    username: \"root\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  const baseSchema = z\n    .object({\n      host: z.string().refine((v) => isHostname(v), t(\"common.errmsg.host_invalid\")),\n      port: z.coerce.number().refine((v) => isPortNumber(v), t(\"common.errmsg.port_invalid\")),\n      authMethod: z.literal([AUTH_METHOD_NONE, AUTH_METHOD_PASSWORD, AUTH_METHOD_KEY], t(\"access.form.ssh_auth_method.placeholder\")),\n      username: z.string().nonempty(t(\"access.form.ssh_username.placeholder\")),\n      password: z.string().nullish(),\n      key: z\n        .string()\n        .max(20480, t(\"common.errmsg.string_max\", { max: 20480 }))\n        .nullish(),\n      keyPassphrase: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.authMethod) {\n        case AUTH_METHOD_PASSWORD:\n          {\n            if (!values.password?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"access.form.ssh_password.placeholder\"),\n                path: [\"password\"],\n              });\n            }\n          }\n          break;\n\n        case AUTH_METHOD_KEY:\n          {\n            if (!values.key?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"access.form.ssh_key.placeholder\"),\n                path: [\"key\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n\n  return baseSchema.safeExtend({\n    jumpServers: z\n      .array(baseSchema, t(\"access.form.ssh_jump_servers.errmsg.invalid\"))\n      .nullish()\n      .refine((v) => {\n        if (v == null) return true;\n        return v.every((item) => baseSchema.safeParse(item).success);\n      }, t(\"access.form.ssh_jump_servers.errmsg.invalid\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderSSH, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderSSLCom.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderSSLCom = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"eabKid\"]} initialValue={initialValues.eabKid} label={t(\"access.form.shared_acme_eab_kid.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_kid.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"eabHmacKey\"]}\n        initialValue={initialValues.eabHmacKey}\n        label={t(\"access.form.shared_acme_eab_hmac_key.label\")}\n        rules={[formRule]}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_hmac_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.sslcom_eab.guide\") }}></span>} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    eabKid: \"\",\n    eabHmacKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    eabKid: z.string().nonempty(t(\"access.form.shared_acme_eab_kid.placeholder\")),\n    eabHmacKey: z.string().nonempty(t(\"access.form.shared_acme_eab_hmac_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderSSLCom, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderSafeLine.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderSafeLine = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.safeline_server_url.label\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.safeline_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiToken\"]}\n        initialValue={initialValues.apiToken}\n        label={t(\"access.form.safeline_api_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.safeline_api_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.safeline_api_token.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:9443/\",\n    apiToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    apiToken: z.string().nonempty(t(\"access.form.safeline_api_token.placeholder\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderSafeLine, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderSectigo.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderSectigo = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"validationType\"]}\n        initialValue={initialValues.validationType}\n        label={t(\"access.form.sectigo_validation_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[\"dv\", \"ov\", \"ev\"].map((s) => ({\n            key: s,\n            label: t(`access.form.sectigo_validation_type.option.${s}.label`),\n            value: s,\n          }))}\n          placeholder={t(\"access.form.sectigo_validation_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"eabKid\"]} initialValue={initialValues.eabKid} label={t(\"access.form.shared_acme_eab_kid.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_kid.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"eabHmacKey\"]}\n        initialValue={initialValues.eabHmacKey}\n        label={t(\"access.form.shared_acme_eab_hmac_key.label\")}\n        rules={[formRule]}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_hmac_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.sectigo_eab.guide\") }}></span>} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    validationType: \"dv\",\n    eabKid: \"\",\n    eabHmacKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    validationType: z.string().nonempty(t(\"access.form.sectigo_validation_type.placeholder\")),\n    eabKid: z.string().nonempty(t(\"access.form.shared_acme_eab_kid.placeholder\")),\n    eabHmacKey: z.string().nonempty(t(\"access.form.shared_acme_eab_hmac_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderSectigo, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderSlackBot.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderSlackBot = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"botToken\"]}\n        initialValue={initialValues.botToken}\n        label={t(\"access.form.slackbot_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.slackbot_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.slackbot_token.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"channelId\"]}\n        initialValue={initialValues.channelId}\n        label={t(\"access.form.slackbot_channel_id.label\")}\n        extra={t(\"access.form.slackbot_channel_id.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.slackbot_channel_id.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"access.form.slackbot_channel_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    botToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    botToken: z.string().nonempty(t(\"access.form.slackbot_token.placeholder\")),\n    channelId: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderSlackBot, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderSpaceship.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderSpaceship = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.spaceship_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.spaceship_api_key.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.spaceship_api_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiSecret\"]}\n        initialValue={initialValues.apiSecret}\n        label={t(\"access.form.spaceship_api_secret.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.spaceship_api_secret.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.spaceship_api_secret.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiKey: \"\",\n    apiSecret: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiKey: z.string().nonempty(t(\"access.form.spaceship_api_key.placeholder\")),\n    apiSecret: z.string().nonempty(t(\"access.form.spaceship_api_secret.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderSpaceship, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderSynologyDSM.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFieldsProviderSynologyDSM = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.synologydsm_server_url.label\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.synologydsm_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"username\"]}\n        initialValue={initialValues.username}\n        label={t(\"access.form.synologydsm_username.label\")}\n        rules={[formRule]}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.synologydsm_username.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"password\"]}\n        initialValue={initialValues.password}\n        label={t(\"access.form.synologydsm_password.label\")}\n        rules={[formRule]}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.synologydsm_password.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"totpSecret\"]}\n        initialValue={initialValues.totpSecret}\n        label={t(\"access.form.synologydsm_totp_secret.label\")}\n        extra={t(\"access.form.synologydsm_totp_secret.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.synologydsm_totp_secret.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.synologydsm_totp_secret.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        rules={[formRule]}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:5000/\",\n    username: \"\",\n    password: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    username: z.string().nonempty(t(\"access.form.synologydsm_username.placeholder\")),\n    password: z.string().nonempty(t(\"access.form.synologydsm_password.placeholder\")),\n    totpSecret: z\n      .string()\n      .nullish()\n      .refine((v) => {\n        if (!v) return true;\n        return /^[A-Z2-7]{16,32}$/.test(v);\n      }),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFieldsProviderSynologyDSM, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderTechnitiumDNS.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderTechnitiumDNS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"serverUrl\"]}\n        initialValue={initialValues.serverUrl}\n        label={t(\"access.form.technitiumdns_server_url.label\")}\n        rules={[formRule]}\n      >\n        <Input type=\"url\" placeholder={t(\"access.form.technitiumdns_server_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiToken\"]}\n        initialValue={initialValues.apiToken}\n        label={t(\"access.form.technitiumdns_api_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.technitiumdns_api_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.technitiumdns_api_token.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    serverUrl: \"http://<your-host-addr>:5380/\",\n    apiToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    serverUrl: z.url(t(\"common.errmsg.url_invalid\")),\n    apiToken: z.string().nonempty(t(\"access.form.technitiumdns_api_token.placeholder\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderTechnitiumDNS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderTelegramBot.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderTelegramBot = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"botToken\"]}\n        initialValue={initialValues.botToken}\n        label={t(\"access.form.telegrambot_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.telegrambot_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.telegrambot_token.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"chatId\"]}\n        initialValue={initialValues.chatId}\n        label={t(\"access.form.telegrambot_chat_id.label\")}\n        extra={t(\"access.form.telegrambot_chat_id.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.telegrambot_chat_id.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"access.form.telegrambot_chat_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    botToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    botToken: z.string().nonempty(t(\"access.form.telegrambot_token.placeholder\")),\n    chatId: z\n      .preprocess(\n        (v) => (v == null || v === \"\" ? void 0 : Number(v)),\n        z.number().refine((v) => {\n          return !Number.isNaN(+v!) && +v! !== 0;\n        }, t(\"access.form.telegrambot_chat_id.placeholder\"))\n      )\n      .nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderTelegramBot, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderTencentCloud.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderTencentCloud = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"secretId\"]}\n        initialValue={initialValues.secretId}\n        label={t(\"access.form.tencentcloud_secret_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.tencentcloud_secret_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.tencentcloud_secret_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretKey\"]}\n        initialValue={initialValues.secretKey}\n        label={t(\"access.form.tencentcloud_secret_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.tencentcloud_secret_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.tencentcloud_secret_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    secretId: \"\",\n    secretKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    secretId: z.string().nonempty(t(\"access.form.tencentcloud_secret_id.placeholder\")),\n    secretKey: z.string().nonempty(t(\"access.form.tencentcloud_secret_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderTencentCloud, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderTodayNIC.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderTodayNIC = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"userId\"]} initialValue={initialValues.userId} label={t(\"access.form.todaynic_user_id.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.todaynic_user_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"apiKey\"]} initialValue={initialValues.apiKey} label={t(\"access.form.todaynic_api_key.label\")} rules={[formRule]}>\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.todaynic_api_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.todaynic_agent.guide\") }}></span>} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    userId: \"\",\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    userId: z.string().nonempty(t(\"access.form.todaynic_user_id.placeholder\")),\n    apiKey: z.string().nonempty(t(\"access.form.todaynic_api_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderTodayNIC, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderUCloud.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderUCloud = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"privateKey\"]}\n        initialValue={initialValues.privateKey}\n        label={t(\"access.form.ucloud_private_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ucloud_private_key.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.ucloud_private_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"publicKey\"]}\n        initialValue={initialValues.publicKey}\n        label={t(\"access.form.ucloud_public_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ucloud_public_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.ucloud_public_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"projectId\"]}\n        initialValue={initialValues.projectId}\n        label={t(\"access.form.ucloud_project_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.ucloud_project_id.tooltip\") }}></span>}\n      >\n        <Input allowClear autoComplete=\"new-password\" placeholder={t(\"access.form.ucloud_project_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    privateKey: \"\",\n    publicKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    privateKey: z.string().nonempty(t(\"access.form.ucloud_private_key.placeholder\")),\n    publicKey: z.string().nonempty(t(\"access.form.ucloud_public_key.placeholder\")),\n    projectId: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderUCloud, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderUniCloud.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderUniCloud = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"username\"]}\n        initialValue={initialValues.username}\n        label={t(\"access.form.unicloud_username.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.unicloud_username.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.unicloud_username.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"password\"]}\n        initialValue={initialValues.password}\n        label={t(\"access.form.unicloud_password.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.unicloud_password.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.unicloud_password.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    username: \"\",\n    password: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    username: z.string().nonempty(t(\"access.form.unicloud_username.placeholder\")),\n    password: z.string().nonempty(t(\"access.form.unicloud_password.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderUniCloud, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderUpyun.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderUpyun = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"username\"]}\n        initialValue={initialValues.username}\n        label={t(\"access.form.upyun_username.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.upyun_username.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.upyun_username.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"password\"]}\n        initialValue={initialValues.password}\n        label={t(\"access.form.upyun_password.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.upyun_password.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.upyun_password.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    username: \"\",\n    password: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    username: z.string().nonempty(t(\"access.form.upyun_username.placeholder\")),\n    password: z.string().nonempty(t(\"access.form.upyun_password.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderUpyun, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderVercel.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderVercel = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiAccessToken\"]}\n        initialValue={initialValues.apiAccessToken}\n        label={t(\"access.form.vercel_api_access_token.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.vercel_api_access_token.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.vercel_api_access_token.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"teamId\"]}\n        initialValue={initialValues.teamId}\n        label={t(\"access.form.vercel_team_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.vercel_team_id.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"access.form.vercel_team_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiAccessToken: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiAccessToken: z.string().nonempty(t(\"access.form.vercel_api_access_token.placeholder\")),\n    teamId: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderVercel, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderVolcEngine.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderVolcEngine = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessKeyId\"]}\n        initialValue={initialValues.accessKeyId}\n        label={t(\"access.form.volcengine_access_key_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.volcengine_access_key_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.volcengine_access_key_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretAccessKey\"]}\n        initialValue={initialValues.secretAccessKey}\n        label={t(\"access.form.volcengine_secret_access_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.volcengine_secret_access_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.volcengine_secret_access_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessKeyId: \"\",\n    secretAccessKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessKeyId: z.string().nonempty(t(\"access.form.volcengine_access_key_id.placeholder\")),\n    secretAccessKey: z.string().nonempty(t(\"access.form.volcengine_secret_access_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderVolcEngine, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderVultr.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderVultr = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.vultr_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.vultr_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.vultr_api_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    apiKey: z.string().nonempty(t(\"access.form.vultr_api_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderVultr, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderWangsu.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderWangsu = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"accessKeyId\"]}\n        initialValue={initialValues.accessKeyId}\n        label={t(\"access.form.wangsu_access_key_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.wangsu_access_key_id.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.wangsu_access_key_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"accessKeySecret\"]}\n        initialValue={initialValues.accessKeySecret}\n        label={t(\"access.form.wangsu_access_key_secret.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.wangsu_access_key_secret.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.wangsu_access_key_secret.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiKey\"]}\n        initialValue={initialValues.apiKey}\n        label={t(\"access.form.wangsu_api_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.wangsu_api_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.wangsu_api_key.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    accessKeyId: \"\",\n    accessKeySecret: \"\",\n    apiKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    accessKeyId: z.string().nonempty(t(\"access.form.wangsu_access_key_id.placeholder\")),\n    accessKeySecret: z.string().nonempty(t(\"access.form.wangsu_access_key_secret.placeholder\")),\n    apiKey: z.string().nonempty(t(\"access.form.wangsu_api_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderWangsu, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderWeComBot.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Checkbox, Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport CodeTextInput from \"@/components/CodeTextInput\";\nimport { isJsonObject } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderWeComBot = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance<z.infer<typeof formSchema>>();\n  const initialValues = getInitialValues();\n\n  const fieldUseCustomPayload = Form.useWatch([parentNamePath, \"useCustomPayload\"], formInst);\n\n  const handleCustomPayloadChecked = (checked: boolean) => {\n    formInst.setFieldValue([parentNamePath, \"useCustomPayload\"], checked);\n    if (checked) {\n      formInst.setFieldValue([parentNamePath, \"customPayload\"], commonPayloadString);\n    } else {\n      formInst.setFieldValue([parentNamePath, \"customPayload\"], void 0);\n    }\n  };\n\n  const handleCustomPayloadBlur = () => {\n    const value = formInst.getFieldValue([parentNamePath, \"customPayload\"]);\n    try {\n      const json = JSON.stringify(JSON.parse(value), null, 2);\n      formInst.setFieldValue([parentNamePath, \"customPayload\"], json);\n    } catch {\n      return;\n    }\n  };\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"webhookUrl\"]}\n        initialValue={initialValues.webhookUrl}\n        label={t(\"access.form.wecombot_webhook_url.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.wecombot_webhook_url.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"access.form.wecombot_webhook_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item label={t(\"access.form.wecombot_custom_payload.label\")}>\n        <Form.Item name={[parentNamePath, \"useCustomPayload\"]} noStyle>\n          <Checkbox checked={!!fieldUseCustomPayload} onChange={(e) => handleCustomPayloadChecked(e.target.checked)}>\n            {t(\"access.form.wecombot_custom_payload.checkbox\")}\n          </Checkbox>\n        </Form.Item>\n        <Form.Item\n          name={[parentNamePath, \"customPayload\"]}\n          hidden={!fieldUseCustomPayload}\n          initialValue={initialValues.customPayload}\n          noStyle\n          rules={[formRule]}\n        >\n          <CodeTextInput\n            className=\"mt-2\"\n            lineWrapping={false}\n            height=\"auto\"\n            minHeight=\"64px\"\n            maxHeight=\"256px\"\n            language=\"json\"\n            placeholder={t(\"access.form.wecombot_custom_payload.placeholder\")}\n            onBlur={handleCustomPayloadBlur}\n          />\n        </Form.Item>\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    webhookUrl: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      webhookUrl: z.url(t(\"common.errmsg.url_invalid\")),\n      useCustomPayload: z.boolean().nullish(),\n      customPayload: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.useCustomPayload) {\n        if (!isJsonObject(values.customPayload!)) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: t(\"common.errmsg.json_invalid\"),\n            path: [\"customPayload\"],\n          });\n        }\n      }\n    });\n};\n\nconst commonPayloadString = JSON.stringify(\n  {\n    msgtype: \"text\",\n    text: {\n      content: \"${CERTIMATE_NOTIFIER_SUBJECT}\\n\\n${CERTIMATE_NOTIFIER_MESSAGE}\",\n    },\n  },\n  null,\n  2\n);\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderWeComBot, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderWebhook.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { IconChevronDown } from \"@tabler/icons-react\";\nimport { Button, Dropdown, Form, Input, Select, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport CodeTextInput from \"@/components/CodeTextInput\";\nimport Show from \"@/components/Show\";\nimport Tips from \"@/components/Tips\";\nimport { isJsonObject } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nexport interface AccessConfigFormFieldsWebhookProps {\n  usage?: \"deployment\" | \"notification\" | \"none\";\n}\n\nconst AccessConfigFormFieldsProviderWebhook = ({ usage = \"none\" }: AccessConfigFormFieldsWebhookProps) => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues({ usage });\n\n  const handleWebhookHeadersBlur = () => {\n    let value = formInst.getFieldValue([parentNamePath, \"headers\"]);\n    value = value.trim();\n    value = value.replace(/(?<!\\r)\\n/g, \"\\r\\n\");\n    formInst.setFieldValue([parentNamePath, \"headers\"], value);\n  };\n\n  const handleWebhookDataForDeploymentBlur = () => {\n    const value = formInst.getFieldValue([parentNamePath, \"data\"]);\n    try {\n      const json = JSON.stringify(JSON.parse(value), null, 2);\n      formInst.setFieldValue([parentNamePath, \"data\"], json);\n    } catch {\n      return;\n    }\n  };\n\n  const handleWebhookDataForNotificationBlur = () => {\n    const value = formInst.getFieldValue([parentNamePath, \"data\"]);\n    try {\n      const json = JSON.stringify(JSON.parse(value), null, 2);\n      formInst.setFieldValue([parentNamePath, \"data\"], json);\n    } catch {\n      return;\n    }\n  };\n\n  const handlePresetDataForDeploymentClick = () => {\n    formInst.setFieldValue([parentNamePath, \"method\"], \"POST\");\n    formInst.setFieldValue([parentNamePath, \"headers\"], \"Content-Type: application/json\");\n    formInst.setFieldValue([parentNamePath, \"data\"], getInitialValues({ usage: \"deployment\" }).data);\n  };\n\n  const handlePresetDataForNotificationClick = (key: string) => {\n    switch (key) {\n      case \"bark\":\n        formInst.setFieldValue([parentNamePath, \"url\"], \"https://api.day.app/push\");\n        formInst.setFieldValue([parentNamePath, \"method\"], \"POST\");\n        formInst.setFieldValue([parentNamePath, \"headers\"], \"Content-Type: application/json\");\n        formInst.setFieldValue(\n          [parentNamePath, \"data\"],\n          JSON.stringify(\n            {\n              title: \"${CERTIMATE_NOTIFIER_SUBJECT}\",\n              body: \"${CERTIMATE_NOTIFIER_MESSAGE}\",\n              device_key: \"<your-bark-device-key>\",\n            },\n            null,\n            2\n          )\n        );\n        break;\n\n      case \"gotify\":\n        formInst.setFieldValue([parentNamePath, \"url\"], \"https://<your-gotify-server>/\");\n        formInst.setFieldValue([parentNamePath, \"method\"], \"POST\");\n        formInst.setFieldValue([parentNamePath, \"headers\"], \"Content-Type: application/json\\r\\nAuthorization: Bearer <your-gotify-token>\");\n        formInst.setFieldValue(\n          [parentNamePath, \"data\"],\n          JSON.stringify(\n            {\n              title: \"${CERTIMATE_NOTIFIER_SUBJECT}\",\n              message: \"${CERTIMATE_NOTIFIER_MESSAGE}\",\n              priority: 1,\n            },\n            null,\n            2\n          )\n        );\n        break;\n\n      case \"messagenest\":\n        formInst.setFieldValue([parentNamePath, \"url\"], \"http://<your-messagenest-server>/api/v1/message/send\");\n        formInst.setFieldValue([parentNamePath, \"method\"], \"POST\");\n        formInst.setFieldValue([parentNamePath, \"headers\"], \"Content-Type: application/json\");\n        formInst.setFieldValue(\n          [parentNamePath, \"data\"],\n          JSON.stringify(\n            {\n              token: \"<your-messagenest-token>\",\n              title: \"${CERTIMATE_NOTIFIER_SUBJECT}\",\n              text: \"${CERTIMATE_NOTIFIER_MESSAGE}\",\n            },\n            null,\n            2\n          )\n        );\n        break;\n\n      case \"ntfy\":\n        formInst.setFieldValue([parentNamePath, \"url\"], \"https://<your-ntfy-server>/\");\n        formInst.setFieldValue([parentNamePath, \"method\"], \"POST\");\n        formInst.setFieldValue([parentNamePath, \"headers\"], \"Content-Type: application/json\");\n        formInst.setFieldValue(\n          [parentNamePath, \"data\"],\n          JSON.stringify(\n            {\n              topic: \"<your-ntfy-topic>\",\n              title: \"${CERTIMATE_NOTIFIER_SUBJECT}\",\n              message: \"${CERTIMATE_NOTIFIER_MESSAGE}\",\n              priority: 1,\n            },\n            null,\n            2\n          )\n        );\n        break;\n\n      case \"pushme\":\n        formInst.setFieldValue([parentNamePath, \"url\"], \"https://push.i-i.me/\");\n        formInst.setFieldValue([parentNamePath, \"method\"], \"POST\");\n        formInst.setFieldValue([parentNamePath, \"headers\"], \"Content-Type: application/json\");\n        formInst.setFieldValue(\n          [parentNamePath, \"data\"],\n          JSON.stringify(\n            {\n              push_key: \"<your-pushme-pushkey>\",\n              type: \"text\",\n              title: \"${CERTIMATE_NOTIFIER_SUBJECT}\",\n              content: \"${CERTIMATE_NOTIFIER_MESSAGE}\",\n            },\n            null,\n            2\n          )\n        );\n        break;\n\n      case \"pushover\":\n        formInst.setFieldValue([parentNamePath, \"url\"], \"https://api.pushover.net/1/messages.json\");\n        formInst.setFieldValue([parentNamePath, \"method\"], \"POST\");\n        formInst.setFieldValue([parentNamePath, \"headers\"], \"Content-Type: application/json\");\n        formInst.setFieldValue(\n          [parentNamePath, \"data\"],\n          JSON.stringify(\n            {\n              token: \"<your-pushover-token>\",\n              user: \"<your-pushover-user>\",\n              title: \"${CERTIMATE_NOTIFIER_SUBJECT}\",\n              message: \"${CERTIMATE_NOTIFIER_MESSAGE}\",\n            },\n            null,\n            2\n          )\n        );\n        break;\n\n      case \"pushplus\":\n        formInst.setFieldValue([parentNamePath, \"url\"], \"https://www.pushplus.plus/send\");\n        formInst.setFieldValue([parentNamePath, \"method\"], \"POST\");\n        formInst.setFieldValue([parentNamePath, \"headers\"], \"Content-Type: application/json\");\n        formInst.setFieldValue(\n          [parentNamePath, \"data\"],\n          JSON.stringify(\n            {\n              token: \"<your-pushplus-token>\",\n              title: \"${CERTIMATE_NOTIFIER_SUBJECT}\",\n              content: \"${CERTIMATE_NOTIFIER_MESSAGE}\",\n            },\n            null,\n            2\n          )\n        );\n        break;\n\n      case \"serverchan3\":\n        formInst.setFieldValue([parentNamePath, \"url\"], \"https://<your-serverchan-uid>.push.ft07.com/send/<your-serverchan-sendkey>.send\");\n        formInst.setFieldValue([parentNamePath, \"method\"], \"POST\");\n        formInst.setFieldValue([parentNamePath, \"headers\"], \"Content-Type: application/json\");\n        formInst.setFieldValue(\n          [parentNamePath, \"data\"],\n          JSON.stringify(\n            {\n              title: \"${CERTIMATE_NOTIFIER_SUBJECT}\",\n              desp: \"${CERTIMATE_NOTIFIER_MESSAGE}\",\n            },\n            null,\n            2\n          )\n        );\n        break;\n\n      case \"serverchanturbo\":\n        formInst.setFieldValue([parentNamePath, \"url\"], \"https://sctapi.ftqq.com/<your-serverchan-key>.send\");\n        formInst.setFieldValue([parentNamePath, \"method\"], \"POST\");\n        formInst.setFieldValue([parentNamePath, \"headers\"], \"Content-Type: application/json\");\n        formInst.setFieldValue(\n          [parentNamePath, \"data\"],\n          JSON.stringify(\n            {\n              title: \"${CERTIMATE_NOTIFIER_SUBJECT}\",\n              desp: \"${CERTIMATE_NOTIFIER_MESSAGE}\",\n            },\n            null,\n            2\n          )\n        );\n        break;\n\n      case \"wxpush\":\n        formInst.setFieldValue([parentNamePath, \"url\"], \"http://<your-wxpush-server>/wxsend\");\n        formInst.setFieldValue([parentNamePath, \"method\"], \"POST\");\n        formInst.setFieldValue([parentNamePath, \"headers\"], \"Content-Type: application/json\\r\\nAuthorization: <your-wxpush-token>\");\n        formInst.setFieldValue(\n          [parentNamePath, \"data\"],\n          JSON.stringify(\n            {\n              title: \"${CERTIMATE_NOTIFIER_SUBJECT}\",\n              content: \"${CERTIMATE_NOTIFIER_MESSAGE}\",\n            },\n            null,\n            2\n          )\n        );\n        break;\n\n      default:\n        formInst.setFieldValue([parentNamePath, \"method\"], \"POST\");\n        formInst.setFieldValue([parentNamePath, \"headers\"], \"Content-Type: application/json\");\n        formInst.setFieldValue([parentNamePath, \"data\"], getInitialValues({ usage: \"notification\" }).data);\n        break;\n    }\n  };\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"url\"]} initialValue={initialValues.url} label={t(\"access.form.webhook_url.label\")} rules={[formRule]}>\n        <Input placeholder={t(\"access.form.webhook_url.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"method\"]} initialValue={initialValues.method} label={t(\"access.form.webhook_method.label\")} rules={[formRule]}>\n        <Select\n          options={[\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"].map((s) => ({ label: s, value: s }))}\n          placeholder={t(\"access.form.webhook_method.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"headers\"]}\n        initialValue={initialValues.headers}\n        label={t(\"access.form.webhook_headers.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.webhook_headers.tooltip\") }}></span>}\n      >\n        <CodeTextInput\n          lineWrapping={false}\n          height=\"auto\"\n          minHeight=\"64px\"\n          maxHeight=\"256px\"\n          placeholder={t(\"access.form.webhook_headers.placeholder\")}\n          onBlur={handleWebhookHeadersBlur}\n        />\n      </Form.Item>\n\n      <Show when={usage === \"deployment\"}>\n        <Form.Item className=\"relative\" label={t(\"access.form.webhook_data.label\")} extra={t(\"access.form.webhook_data.help\")}>\n          <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n            <Dropdown\n              menu={{\n                items: [\n                  {\n                    key: \"common\",\n                    label: t(\"access.form.webhook_preset_data.common\"),\n                    onClick: handlePresetDataForDeploymentClick,\n                  },\n                ],\n              }}\n              trigger={[\"click\"]}\n            >\n              <Button size=\"small\" type=\"link\">\n                {t(\"access.form.webhook_preset_data\")}\n                <IconChevronDown size=\"1.25em\" />\n              </Button>\n            </Dropdown>\n          </div>\n          <Form.Item name={[parentNamePath, \"data\"]} initialValue={initialValues.data} noStyle rules={[formRule]}>\n            <CodeTextInput\n              lineWrapping={false}\n              height=\"auto\"\n              minHeight=\"64px\"\n              maxHeight=\"256px\"\n              language=\"json\"\n              placeholder={t(\"access.form.webhook_data.placeholder\")}\n              onBlur={handleWebhookDataForDeploymentBlur}\n            />\n          </Form.Item>\n        </Form.Item>\n\n        <Form.Item>\n          <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.webhook_data.guide_for_deployment\") }}></span>} />\n        </Form.Item>\n      </Show>\n\n      <Show when={usage === \"notification\"}>\n        <Form.Item className=\"relative\" label={t(\"access.form.webhook_data.label\")} extra={t(\"access.form.webhook_data.help\")}>\n          <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n            <Dropdown\n              menu={{\n                items: [\"bark\", \"ntfy\", \"gotify\", \"serverchan3\", \"serverchanturbo\", \"pushover\", \"pushplus\", \"messagenest\", \"wxpush\", \"pushme\", \"common\"].map(\n                  (key) => ({\n                    key,\n                    label: <span dangerouslySetInnerHTML={{ __html: t(`access.form.webhook_preset_data.${key}`) }}></span>,\n                    onClick: () => handlePresetDataForNotificationClick(key),\n                  })\n                ),\n              }}\n              trigger={[\"click\"]}\n            >\n              <Button size=\"small\" type=\"link\">\n                {t(\"access.form.webhook_preset_data\")}\n                <IconChevronDown size=\"1.25em\" />\n              </Button>\n            </Dropdown>\n          </div>\n          <Form.Item name={[parentNamePath, \"data\"]} initialValue={initialValues.data} noStyle rules={[formRule]}>\n            <CodeTextInput\n              lineWrapping={false}\n              height=\"auto\"\n              minHeight=\"64px\"\n              maxHeight=\"256px\"\n              language=\"json\"\n              placeholder={t(\"access.form.webhook_data.placeholder\")}\n              onBlur={handleWebhookDataForNotificationBlur}\n            />\n          </Form.Item>\n        </Form.Item>\n\n        <Form.Item>\n          <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.webhook_data.guide_for_notification\") }}></span>} />\n        </Form.Item>\n      </Show>\n\n      <Form.Item\n        name={[parentNamePath, \"allowInsecureConnections\"]}\n        initialValue={initialValues.allowInsecureConnections}\n        label={t(\"access.form.shared_allow_insecure_conns.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = ({ usage = \"none\" }: { usage?: \"deployment\" | \"notification\" | \"none\" }): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    url: \"\",\n    method: \"POST\",\n    headers: \"Content-Type: application/json\",\n    allowInsecureConnections: false,\n    data: JSON.stringify(\n      usage === \"deployment\"\n        ? {\n            name: \"${CERTIMATE_DEPLOYER_COMMONNAME}\",\n            cert: \"${CERTIMATE_DEPLOYER_CERTIFICATE}\",\n            privkey: \"${CERTIMATE_DEPLOYER_PRIVATEKEY}\",\n          }\n        : usage === \"notification\"\n          ? {\n              subject: \"${CERTIMATE_NOTIFIER_SUBJECT}\",\n              message: \"${CERTIMATE_NOTIFIER_MESSAGE}\",\n            }\n          : {},\n      null,\n      2\n    ),\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    url: z.url(t(\"common.errmsg.url_invalid\")),\n    method: z.literal([\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"], t(\"access.form.webhook_method.placeholder\")),\n    headers: z\n      .string()\n      .nullish()\n      .refine((v) => {\n        if (!v) return true;\n\n        const lines = v.split(/\\r?\\n/);\n        for (const line of lines) {\n          if (line.split(\":\").length < 2) {\n            return false;\n          }\n        }\n        return true;\n      }, t(\"access.form.webhook_headers.errmsg.invalid\")),\n    data: z\n      .string()\n      .nullish()\n      .refine((v) => {\n        if (!v) return true;\n        return isJsonObject(v);\n      }, t(\"common.errmsg.json_invalid\")),\n    allowInsecureConnections: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderWebhook, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderWestcn.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderWestcn = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"username\"]} initialValue={initialValues.username} label={t(\"access.form.westcn_username.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.westcn_username.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiPassword\"]}\n        initialValue={initialValues.apiPassword}\n        label={t(\"access.form.westcn_api_password.label\")}\n        rules={[formRule]}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.westcn_api_password.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.westcn_agent.guide\") }}></span>} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    username: \"\",\n    apiPassword: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    username: z.string().nonempty(t(\"access.form.westcn_username.placeholder\")),\n    apiPassword: z.string().nonempty(t(\"access.form.westcn_api_password.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderWestcn, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderXinnet.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderXinnet = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"agentId\"]} initialValue={initialValues.agentId} label={t(\"access.form.xinnet_agent_id.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.xinnet_agent_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"apiPassword\"]}\n        initialValue={initialValues.apiPassword}\n        label={t(\"access.form.xinnet_api_password.label\")}\n        rules={[formRule]}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.xinnet_api_password.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.xinnet_agent.guide\") }}></span>} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    agentId: \"\",\n    apiPassword: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    agentId: z.string().nonempty(t(\"access.form.xinnet_agent_id.placeholder\")),\n    apiPassword: z.string().nonempty(t(\"access.form.xinnet_api_password.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderXinnet, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/AccessConfigFieldsProviderZeroSSL.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst AccessConfigFormFieldsProviderZeroSSL = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"eabKid\"]} initialValue={initialValues.eabKid} label={t(\"access.form.shared_acme_eab_kid.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_kid.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"eabHmacKey\"]}\n        initialValue={initialValues.eabHmacKey}\n        label={t(\"access.form.shared_acme_eab_hmac_key.label\")}\n        rules={[formRule]}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_hmac_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.zerossl_eab.guide\") }}></span>} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    eabKid: \"\",\n    eabHmacKey: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    eabKid: z.string().nonempty(t(\"access.form.shared_acme_eab_kid.placeholder\")),\n    eabHmacKey: z.string().nonempty(t(\"access.form.shared_acme_eab_hmac_key.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(AccessConfigFormFieldsProviderZeroSSL, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/access/forms/_context.ts",
    "content": "﻿import { createContext, useContext } from \"react\";\n\n// #region FormNestedFieldsContext\nexport type FormNestedFieldsContextType = {\n  parentNamePath: string;\n};\n\nexport const FormNestedFieldsContext = createContext<FormNestedFieldsContextType>({\n  parentNamePath: \"\",\n});\n\nexport const FormNestedFieldsContextProvider = FormNestedFieldsContext.Provider;\n\nexport const useFormNestedFieldsContext = () => {\n  const context = useContext(FormNestedFieldsContext);\n  if (!context) {\n    throw new Error(\"`FormNestedFieldsContext` must be used within a `FormNestedFieldsContextProvider`\");\n  }\n  return context;\n};\n// #endregion\n"
  },
  {
    "path": "ui/src/components/access/forms/_hooks.ts",
    "content": "﻿import { useMemo } from \"react\";\n\nimport { ACCESS_USAGES, type AccessProvider } from \"@/domain/provider\";\n\nexport const useProviderFilterByUsage = (usage?: \"dns\" | \"hosting\" | \"dns-hosting\" | \"ca\" | \"notification\") => {\n  return useMemo(() => {\n    if (usage == null) return;\n\n    switch (usage) {\n      case \"dns\":\n        return (_: string, option: AccessProvider) => option.usages.includes(ACCESS_USAGES.DNS);\n      case \"hosting\":\n        return (_: string, option: AccessProvider) => option.usages.includes(ACCESS_USAGES.HOSTING);\n      case \"dns-hosting\":\n        return (_: string, option: AccessProvider) => option.usages.includes(ACCESS_USAGES.DNS) || option.usages.includes(ACCESS_USAGES.HOSTING);\n      case \"ca\":\n        return (_: string, option: AccessProvider) => option.usages.includes(ACCESS_USAGES.CA);\n      case \"notification\":\n        return (_: string, option: AccessProvider) => option.usages.includes(ACCESS_USAGES.NOTIFICATION);\n      default:\n        console.warn(`[certimate] unsupported provider usage: '${usage}'`);\n    }\n  }, [usage]);\n};\n"
  },
  {
    "path": "ui/src/components/certificate/CertificateDetail.tsx",
    "content": "import { CopyToClipboard } from \"react-copy-to-clipboard\";\nimport { useTranslation } from \"react-i18next\";\nimport { IconClipboard, IconDownload, IconThumbUp } from \"@tabler/icons-react\";\nimport { App, Button, Dropdown, Form, Input, Tag, Tooltip } from \"antd\";\nimport dayjs from \"dayjs\";\nimport { saveAs } from \"file-saver\";\n\nimport { download as downloadCertificate } from \"@/api/certificates\";\nimport { CERTIFICATE_FORMATS, type CertificateFormatType, type CertificateModel } from \"@/domain/certificate\";\n\nexport interface CertificateDetailProps {\n  className?: string;\n  style?: React.CSSProperties;\n  data: CertificateModel;\n}\n\nconst CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {\n  const { t } = useTranslation();\n\n  const { message } = App.useApp();\n\n  const handleDownloadClick = async (format: CertificateFormatType) => {\n    try {\n      const res = await downloadCertificate(data.id, format);\n      const bstr = atob(res.data.fileBytes);\n      const u8arr = Uint8Array.from(bstr, (ch) => ch.charCodeAt(0));\n      const blob = new Blob([u8arr], { type: \"application/zip\" });\n      saveAs(blob, `${data.id}-${data.subjectAltNames}.zip`);\n    } catch (err) {\n      console.error(err);\n      message.warning(t(\"common.text.operation_failed\"));\n    }\n  };\n\n  return (\n    <div {...props}>\n      <Form layout=\"vertical\">\n        <Form.Item label={t(\"certificate.props.subject_alt_names\")}>\n          <Input value={data.subjectAltNames} variant=\"filled\" placeholder=\"\" />\n        </Form.Item>\n\n        <Form.Item label={t(\"certificate.props.issuer\")}>\n          <Input value={data.issuerOrg} variant=\"filled\" placeholder=\"\" />\n        </Form.Item>\n\n        <Form.Item label={t(\"certificate.props.validity\")}>\n          <Input\n            value={`${dayjs(data.validityNotBefore).format(\"YYYY-MM-DD HH:mm:ss\")} ~ ${dayjs(data.validityNotAfter).format(\"YYYY-MM-DD HH:mm:ss\")}`}\n            variant=\"filled\"\n            placeholder=\"\"\n            suffix={data.isRevoked ? <Tag color=\"error\">{t(\"certificate.props.revoked\")}</Tag> : <></>}\n          />\n        </Form.Item>\n\n        <Form.Item label={t(\"certificate.props.serial_number\")}>\n          <Input value={data.serialNumber} variant=\"filled\" placeholder=\"\" />\n        </Form.Item>\n\n        <Form.Item label={t(\"certificate.props.key_algorithm\")}>\n          <Input value={data.keyAlgorithm} variant=\"filled\" placeholder=\"\" />\n        </Form.Item>\n\n        <Form.Item label={t(\"certificate.props.certificate\")}>\n          <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n            <Tooltip title={t(\"common.button.copy\")}>\n              <CopyToClipboard\n                text={data.certificate}\n                onCopy={() => {\n                  message.success(t(\"common.text.copied\"));\n                }}\n              >\n                <Button size=\"small\" type=\"text\" icon={<IconClipboard size=\"1.25em\" />}></Button>\n              </CopyToClipboard>\n            </Tooltip>\n          </div>\n          <Input.TextArea value={data.certificate} variant=\"filled\" autoSize={{ minRows: 5, maxRows: 5 }} readOnly />\n        </Form.Item>\n\n        <Form.Item label={t(\"certificate.props.private_key\")}>\n          <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n            <Tooltip title={t(\"common.button.copy\")}>\n              <CopyToClipboard\n                text={data.privateKey}\n                onCopy={() => {\n                  message.success(t(\"common.text.copied\"));\n                }}\n              >\n                <Button size=\"small\" type=\"text\" icon={<IconClipboard size=\"1.25em\" />}></Button>\n              </CopyToClipboard>\n            </Tooltip>\n          </div>\n          <Input.TextArea value={data.privateKey} variant=\"filled\" autoSize={{ minRows: 5, maxRows: 5 }} readOnly />\n        </Form.Item>\n      </Form>\n\n      <div className=\"flex items-center justify-end\">\n        <Dropdown\n          menu={{\n            items: [\n              {\n                key: \"PEM\",\n                label: \"PEM\",\n                extra: <IconThumbUp size=\"1.25em\" />,\n                onClick: () => handleDownloadClick(CERTIFICATE_FORMATS.PEM),\n              },\n              {\n                key: \"PFX\",\n                label: \"PFX\",\n                onClick: () => handleDownloadClick(CERTIFICATE_FORMATS.PFX),\n              },\n              {\n                key: \"JKS\",\n                label: \"JKS\",\n                onClick: () => handleDownloadClick(CERTIFICATE_FORMATS.JKS),\n              },\n            ],\n          }}\n          trigger={[\"click\", \"hover\"]}\n        >\n          <Button icon={<IconDownload size=\"1.25em\" />} type=\"primary\">\n            {t(\"common.button.download\")}\n          </Button>\n        </Dropdown>\n      </div>\n    </div>\n  );\n};\n\nexport default CertificateDetail;\n"
  },
  {
    "path": "ui/src/components/certificate/CertificateDetailDrawer.tsx",
    "content": "import { startTransition, useCallback, useState } from \"react\";\nimport { IconX } from \"@tabler/icons-react\";\nimport { useControllableValue, useGetState } from \"ahooks\";\nimport { Button, Drawer, Flex } from \"antd\";\n\nimport Show from \"@/components/Show\";\nimport { type CertificateModel } from \"@/domain/certificate\";\nimport { useTriggerElement } from \"@/hooks\";\nimport CertificateDetail from \"./CertificateDetail\";\n\nexport interface CertificateDetailDrawerProps {\n  afterClose?: () => void;\n  data?: CertificateModel;\n  loading?: boolean;\n  open?: boolean;\n  trigger?: React.ReactNode;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst CertificateDetailDrawer = ({ afterClose, data, loading, trigger, ...props }: CertificateDetailDrawerProps) => {\n  const [open, setOpen] = useControllableValue<boolean>(props, {\n    valuePropName: \"open\",\n    defaultValuePropName: \"defaultOpen\",\n    trigger: \"onOpenChange\",\n  });\n\n  const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) });\n\n  return (\n    <>\n      {triggerEl}\n\n      <Drawer\n        afterOpenChange={(open) => !open && afterClose?.()}\n        autoFocus\n        closeIcon={false}\n        destroyOnHidden\n        open={open}\n        loading={loading}\n        placement=\"right\"\n        size=\"large\"\n        title={\n          <Flex align=\"center\" justify=\"space-between\" gap=\"small\">\n            <div className=\"flex-1 truncate\">{data ? `Certificate #${data.id}` : \"Certificate\"}</div>\n            <Button\n              className=\"ant-drawer-close\"\n              style={{ marginInline: 0 }}\n              icon={<IconX size=\"1.25em\" />}\n              size=\"small\"\n              type=\"text\"\n              onClick={() => setOpen(false)}\n            />\n          </Flex>\n        }\n        onClose={() => setOpen(false)}\n      >\n        <Show when={!!data}>\n          <CertificateDetail data={data!} />\n        </Show>\n      </Drawer>\n    </>\n  );\n};\n\nconst useDrawer = () => {\n  type DataType = CertificateDetailDrawerProps[\"data\"];\n  const [data, setData, getData] = useGetState<DataType>();\n  const [loading, setLoading] = useState<boolean>();\n  const [open, setOpen] = useState(false);\n\n  const onOpenChange = useCallback((open: boolean) => {\n    setOpen(open);\n  }, []);\n\n  return {\n    drawerProps: {\n      afterClose: () => {\n        startTransition(() => {\n          if (!open) {\n            setData(void 0);\n            setLoading(void 0);\n          }\n        });\n      },\n      data,\n      loading,\n      open,\n      onOpenChange,\n    },\n\n    open: ({ data, loading }: { data: NonNullable<DataType>; loading?: boolean }) => {\n      setData(data);\n      setLoading(loading);\n      setOpen(true);\n\n      return {\n        safeUpdate: ({ data, loading }: { data?: NonNullable<DataType>; loading?: boolean }) => {\n          if (data != null) {\n            if (data.id !== getData()?.id) return; // 确保数据不脏读\n\n            setData(data);\n          }\n\n          if (loading != null) {\n            setLoading(loading);\n          }\n        },\n      };\n    },\n    close: () => {\n      setOpen(false);\n    },\n  };\n};\n\nconst _default = Object.assign(CertificateDetailDrawer, {\n  useDrawer,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/icons/IconLanguageEnZh.tsx",
    "content": "﻿import createIconComponent from \"./createIconComponent\";\n\nconst IconLanguageEnZh = createIconComponent(\n  \"filled\",\n  \"IconLanguageEnZh\",\n  {\n    viewBox: \"0 0 1024 1024\",\n  },\n  [\n    [\n      \"path\",\n      {\n        key: \"svg-0\",\n        d: \"M279.04 811.52c-43.52-2.56-76.8-38.4-76.8-81.92v-35.84H143.36v43.52c5.12 74.24 66.56 133.12 140.8 133.12h35.84v-58.88H279.04zM734.72 204.8c43.52 2.56 76.8 38.4 76.8 81.92v35.84H870.4v-43.52c-5.12-74.24-66.56-133.12-140.8-133.12h-35.84V204.8H734.72z\",\n      },\n    ],\n    [\n      \"path\",\n      {\n        key: \"svg-1\",\n        d: \"M998.4 565.76c-2.56-74.24-64-130.56-138.24-130.56H581.12V163.84c0-76.8-61.44-138.24-138.24-138.24H163.84C87.04 25.6 25.6 87.04 25.6 163.84v279.04c0 76.8 61.44 138.24 138.24 138.24H435.2v279.04c0 76.8 61.44 138.24 138.24 138.24h289.28c76.8 0 138.24-61.44 138.24-138.24V573.44l-2.56-7.68zM304.64 435.2H128V153.6h171.52v46.08H184.32v64H281.6v46.08H184.32V384h120.32v51.2z m151.04 0v-125.44c0-33.28-10.24-43.52-30.72-43.52-17.92 0-28.16 7.68-43.52 23.04V435.2h-56.32V222.72h46.08l2.56 28.16h2.56c17.92-17.92 40.96-33.28 69.12-33.28 46.08 0 66.56 30.72 66.56 84.48V435.2h-56.32z m483.84 424.96c0 20.48-7.68 40.96-23.04 56.32s-35.84 23.04-56.32 23.04H573.44c-20.48 0-40.96-7.68-56.32-23.04s-23.04-35.84-23.04-56.32V573.44c35.84-15.36 64-43.52 79.36-79.36h289.28c43.52 0 79.36 35.84 79.36 79.36v286.72z\",\n      },\n    ],\n    [\n      \"path\",\n      {\n        key: \"svg-2\",\n        d: \"M760.32 545.28h-46.08v61.44H588.8v179.2h43.52V768h81.92v112.64h46.08V768h81.92v17.92h46.08v-179.2h-125.44v-61.44z m-46.08 181.76h-81.92v-76.8h81.92v76.8z m128-76.8v76.8h-81.92v-76.8h81.92z\",\n      },\n    ],\n  ]\n);\n\nexport default IconLanguageEnZh;\n"
  },
  {
    "path": "ui/src/components/icons/IconLanguageZhEn.tsx",
    "content": "﻿import createIconComponent from \"./createIconComponent\";\n\nconst IconLanguageZhEn = createIconComponent(\n  \"filled\",\n  \"IconLanguageZhEn\",\n  {\n    viewBox: \"0 0 1024 1024\",\n  },\n  [\n    [\n      \"path\",\n      {\n        key: \"svg-0\",\n        d: \"M199.68 240.64H281.6v76.8H199.68zM279.04 811.52c-43.52-2.56-76.8-38.4-76.8-81.92v-35.84H143.36v43.52c5.12 74.24 66.56 133.12 140.8 133.12h35.84v-58.88H279.04zM734.72 204.8c43.52 2.56 76.8 38.4 76.8 81.92v35.84H870.4v-43.52c-5.12-74.24-66.56-133.12-140.8-133.12h-35.84V204.8H734.72zM593.92 747.52H691.2v-46.08h-97.28v-66.56h115.2V588.8H537.6v281.6h176.64v-46.08h-120.32zM325.12 240.64h81.92v76.8h-81.92z\",\n      },\n    ],\n    [\n      \"path\",\n      {\n        key: \"svg-1\",\n        d: \"M998.4 565.76c-2.56-74.24-64-130.56-138.24-130.56H581.12V163.84c0-76.8-61.44-138.24-138.24-138.24H163.84C87.04 25.6 25.6 87.04 25.6 163.84v279.04c0 76.8 61.44 138.24 138.24 138.24H435.2v279.04c0 76.8 61.44 138.24 138.24 138.24h289.28c76.8 0 138.24-61.44 138.24-138.24V573.44l-2.56-7.68z m-547.84-189.44h-46.08V358.4h-81.92v112.64h-46.08V358.4h-76.8v20.48H156.16v-179.2H281.6V135.68h46.08v61.44h125.44v179.2z m488.96 483.84c0 20.48-7.68 40.96-23.04 56.32s-35.84 23.04-56.32 23.04H573.44c-20.48 0-40.96-7.68-56.32-23.04s-23.04-35.84-23.04-56.32V573.44c35.84-15.36 64-43.52 79.36-79.36h289.28c43.52 0 79.36 35.84 79.36 79.36v286.72z\",\n      },\n    ],\n    [\n      \"path\",\n      {\n        key: \"svg-2\",\n        d: \"M855.04 652.8c-28.16 0-51.2 15.36-69.12 33.28h-2.56l-2.56-28.16h-46.08V870.4h56.32v-145.92c15.36-15.36 28.16-23.04 43.52-23.04 20.48 0 30.72 12.8 30.72 43.52V870.4H921.6v-133.12c0-53.76-20.48-84.48-66.56-84.48z\",\n      },\n    ],\n  ]\n);\n\nexport default IconLanguageZhEn;\n"
  },
  {
    "path": "ui/src/components/icons/createIconComponent.ts",
    "content": "﻿import { createElement, forwardRef } from \"react\";\nimport { type Icon, type IconNode, type IconProps } from \"@tabler/icons-react\";\n\nconst defaultAttrs = {\n  outline: {\n    xmlns: \"http://www.w3.org/2000/svg\",\n    width: 24,\n    height: 24,\n    viewBox: \"0 0 24 24\",\n    fill: \"none\",\n    stroke: \"currentColor\",\n    strokeWidth: 2,\n    strokeLinecap: \"round\",\n    strokeLinejoin: \"round\",\n  },\n  filled: {\n    xmlns: \"http://www.w3.org/2000/svg\",\n    width: 24,\n    height: 24,\n    viewBox: \"0 0 24 24\",\n    fill: \"currentColor\",\n    stroke: \"none\",\n  },\n};\n\nconst createIconComponent = (type: \"outline\" | \"filled\", iconName: string, iconAttrs: Record<string, any>, iconNode: IconNode) => {\n  const Component = forwardRef<Icon, IconProps>(({ color = \"currentColor\", size = 24, stroke = 2, title, className, children, ...rest }: IconProps, ref) =>\n    createElement(\n      \"svg\",\n      {\n        ref,\n        ...defaultAttrs[type],\n        ...iconAttrs,\n        width: size,\n        height: size,\n        className: [\"icon\", className],\n        ...(type === \"filled\"\n          ? {\n              fill: color,\n            }\n          : {\n              strokeWidth: stroke,\n              stroke: color,\n            }),\n        ...rest,\n      },\n      [\n        title && createElement(\"title\", { key: \"svg-title\" }, title),\n        ...iconNode.map(([tag, attrs]) => createElement(tag, attrs)),\n        ...(Array.isArray(children) ? children : [children]),\n      ]\n    )\n  );\n\n  Component.displayName = iconName;\n\n  return Component;\n};\n\nexport default createIconComponent;\n"
  },
  {
    "path": "ui/src/components/icons/index.ts",
    "content": "﻿import IconLanguageEnZh from \"./IconLanguageEnZh.tsx\";\nimport IconLanguageZhEn from \"./IconLanguageZhEn.tsx\";\n\nexport { IconLanguageEnZh, IconLanguageZhEn };\n"
  },
  {
    "path": "ui/src/components/preset/PresetNotifyTemplatesPopselect.tsx",
    "content": "﻿import { useMemo } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { IconMoodEmpty } from \"@tabler/icons-react\";\nimport { useMount } from \"ahooks\";\nimport { Dropdown, type DropdownProps } from \"antd\";\n\nimport { useZustandShallowSelector } from \"@/hooks\";\nimport { useNotifyTemplatesStore } from \"@/stores/settings\";\n\ntype PresetTemplate = {\n  subject: string;\n  message: string;\n};\n\nexport interface PresetNotifyTemplatesPopselectProps extends Omit<DropdownProps, \"menu\"> {\n  options?: NonNullable<DropdownProps[\"menu\"]>[\"items\"];\n  onSelect?: (value: string, template?: PresetTemplate | undefined) => void;\n}\n\nconst PresetNotifyTemplatesPopselect = ({ className, options, onSelect, ...props }: PresetNotifyTemplatesPopselectProps) => {\n  const { t } = useTranslation();\n\n  const { templates, fetchTemplates } = useNotifyTemplatesStore(useZustandShallowSelector([\"templates\", \"fetchTemplates\"]));\n  useMount(() => {\n    fetchTemplates(false);\n  });\n\n  const menuItems = useMemo(() => {\n    type MenuItem = NonNullable<NonNullable<DropdownProps[\"menu\"]>[\"items\"]>[number];\n    const temp: MenuItem[] = [];\n\n    if (!options?.length && !templates?.length) {\n      temp.push({\n        key: \"nodata\",\n        label: t(\"common.text.nodata\"),\n        icon: (\n          <span className=\"anticon scale-125\">\n            <IconMoodEmpty size=\"1em\" />\n          </span>\n        ),\n        disabled: true,\n      });\n      return temp;\n    }\n\n    if (options?.length) {\n      temp.push(\n        ...options.map((option) => {\n          return {\n            ...option!,\n            onClick: (e: any) => {\n              if (\"onClick\" in option!) {\n                option.onClick?.(e);\n              }\n\n              onSelect?.(String(option!.key!));\n            },\n          };\n        })\n      );\n    }\n\n    if (templates?.length) {\n      temp.push({\n        key: \"custom\",\n        label: t(\"preset.dropdown.option_group.custom\"),\n        children: templates.map((template) => ({\n          key: `custom/${template.name}`,\n          label: template.name,\n          onClick: () => {\n            onSelect?.(template.name, template);\n          },\n        })),\n      });\n    }\n\n    return temp;\n  }, [options, templates, onSelect]);\n\n  return <Dropdown className={className} menu={{ items: menuItems }} {...props} />;\n};\n\nexport default PresetNotifyTemplatesPopselect;\n"
  },
  {
    "path": "ui/src/components/preset/PresetScriptTemplatesPopselect.tsx",
    "content": "﻿import { useMemo } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { IconMoodEmpty } from \"@tabler/icons-react\";\nimport { useMount } from \"ahooks\";\nimport { Dropdown, type DropdownProps } from \"antd\";\n\nimport { useZustandShallowSelector } from \"@/hooks\";\nimport { useScriptTemplatesStore } from \"@/stores/settings\";\n\ntype PresetTemplate = {\n  command: string;\n};\n\nexport interface PresetScriptTemplatesPopselectProps extends Omit<DropdownProps, \"menu\"> {\n  options?: NonNullable<DropdownProps[\"menu\"]>[\"items\"];\n  onSelect?: (value: string, template?: PresetTemplate | undefined) => void;\n}\n\nconst PresetScriptTemplatesPopselect = ({ className, options, onSelect, ...props }: PresetScriptTemplatesPopselectProps) => {\n  const { t } = useTranslation();\n\n  const { templates, fetchTemplates } = useScriptTemplatesStore(useZustandShallowSelector([\"templates\", \"fetchTemplates\"]));\n  useMount(() => {\n    fetchTemplates(false);\n  });\n\n  const menuItems = useMemo(() => {\n    type MenuItem = NonNullable<NonNullable<DropdownProps[\"menu\"]>[\"items\"]>[number];\n    const temp: MenuItem[] = [];\n\n    if (!options?.length && !templates?.length) {\n      temp.push({\n        key: \"nodata\",\n        label: t(\"common.text.nodata\"),\n        icon: (\n          <span className=\"anticon scale-125\">\n            <IconMoodEmpty size=\"1em\" />\n          </span>\n        ),\n        disabled: true,\n      });\n      return temp;\n    }\n\n    if (options?.length) {\n      temp.push(\n        ...options.map((option) => {\n          return {\n            ...option!,\n            onClick: (e: any) => {\n              if (\"onClick\" in option!) {\n                option.onClick?.(e);\n              }\n\n              onSelect?.(String(option!.key!));\n            },\n          };\n        })\n      );\n    }\n\n    if (templates?.length) {\n      temp.push({\n        key: \"custom\",\n        label: t(\"preset.dropdown.option_group.custom\"),\n        children: templates.map((template) => ({\n          key: `custom/${template.name}`,\n          label: template.name,\n          onClick: () => {\n            onSelect?.(template.name, template);\n          },\n        })),\n      });\n    }\n\n    return temp;\n  }, [options, templates, onSelect]);\n\n  return <Dropdown className={className} menu={{ items: menuItems }} {...props} />;\n};\n\nexport default PresetScriptTemplatesPopselect;\n"
  },
  {
    "path": "ui/src/components/provider/ACMEDns01ProviderSelect.tsx",
    "content": "import { useMemo } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Avatar, Select, Typography, theme } from \"antd\";\n\nimport { type ACMEDns01Provider, acmeDns01ProvidersMap } from \"@/domain/provider\";\nimport { matchSearchOption } from \"@/utils/search\";\n\nimport { type SharedSelectProps, useSelectDataSource } from \"./_shared\";\n\nexport interface ACMEDns01ProviderSelectProps extends SharedSelectProps<ACMEDns01Provider> {\n  showAvailability?: boolean;\n}\n\nconst ACMEDns01ProviderSelect = ({ showAvailability, onFilter, ...props }: ACMEDns01ProviderSelectProps) => {\n  const { t } = useTranslation();\n\n  const { token: themeToken } = theme.useToken();\n\n  const dataSources = useSelectDataSource({\n    dataSource: Array.from(acmeDns01ProvidersMap.values()),\n    filters: [onFilter!],\n  });\n  const options = useMemo(() => {\n    const convert = (providers: ACMEDns01Provider[]): Array<{ key: string; value: string; label: string; data: ACMEDns01Provider }> => {\n      return providers.map((provider) => ({\n        key: provider.type,\n        value: provider.type,\n        label: t(provider.name),\n        data: provider,\n      }));\n    };\n\n    return showAvailability\n      ? [\n          {\n            label: t(\"provider.text.available_group\"),\n            options: convert(dataSources.available),\n          },\n          {\n            label: t(\"provider.text.unavailable_group\"),\n            options: convert(dataSources.unavailable),\n          },\n        ].filter((group) => group.options.length > 0)\n      : convert(dataSources.filtered);\n  }, [showAvailability, dataSources]);\n\n  const renderOption = (key: string) => {\n    const provider = acmeDns01ProvidersMap.get(key);\n    return (\n      <div className=\"flex items-center gap-2 truncate overflow-hidden\">\n        <Avatar shape=\"square\" src={provider?.icon} size=\"small\" />\n        <Typography.Text ellipsis>{t(provider?.name ?? \"\")}</Typography.Text>\n      </div>\n    );\n  };\n\n  return (\n    <Select\n      {...props}\n      labelRender={({ value }) => {\n        if (value != null) {\n          return renderOption(value as string);\n        }\n\n        return <span style={{ color: themeToken.colorTextPlaceholder }}>{props.placeholder}</span>;\n      }}\n      options={options}\n      optionLabelProp={void 0}\n      optionRender={(option) => renderOption(option.data.value as string)}\n      showSearch={{\n        filterOption: (inputValue, option) => matchSearchOption(inputValue, option!),\n      }}\n    />\n  );\n};\n\nexport default ACMEDns01ProviderSelect;\n"
  },
  {
    "path": "ui/src/components/provider/ACMEHttp01ProviderSelect.tsx",
    "content": "import { useMemo } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Avatar, Select, Typography, theme } from \"antd\";\n\nimport { type ACMEHttp01Provider, acmeHttp01ProvidersMap } from \"@/domain/provider\";\nimport { matchSearchOption } from \"@/utils/search\";\n\nimport { type SharedSelectProps, useSelectDataSource } from \"./_shared\";\n\nexport interface ACMEHttp01ProviderSelectProps extends SharedSelectProps<ACMEHttp01Provider> {\n  showAvailability?: boolean;\n}\n\nconst ACMEHttp01ProviderSelect = ({ showAvailability, onFilter, ...props }: ACMEHttp01ProviderSelectProps) => {\n  const { t } = useTranslation();\n\n  const { token: themeToken } = theme.useToken();\n\n  const dataSources = useSelectDataSource({\n    dataSource: Array.from(acmeHttp01ProvidersMap.values()),\n    filters: [onFilter!],\n  });\n  const options = useMemo(() => {\n    const convert = (providers: ACMEHttp01Provider[]): Array<{ key: string; value: string; label: string; data: ACMEHttp01Provider }> => {\n      return providers.map((provider) => ({\n        key: provider.type,\n        value: provider.type,\n        label: t(provider.name),\n        data: provider,\n      }));\n    };\n\n    return showAvailability\n      ? [\n          {\n            label: t(\"provider.text.available_group\"),\n            options: convert(dataSources.available),\n          },\n          {\n            label: t(\"provider.text.unavailable_group\"),\n            options: convert(dataSources.unavailable),\n          },\n        ].filter((group) => group.options.length > 0)\n      : convert(dataSources.filtered);\n  }, [showAvailability, dataSources]);\n\n  const renderOption = (key: string) => {\n    const provider = acmeHttp01ProvidersMap.get(key);\n    return (\n      <div className=\"flex items-center gap-2 truncate overflow-hidden\">\n        <Avatar shape=\"square\" src={provider?.icon} size=\"small\" />\n        <Typography.Text ellipsis>{t(provider?.name ?? \"\")}</Typography.Text>\n      </div>\n    );\n  };\n\n  return (\n    <Select\n      {...props}\n      labelRender={({ value }) => {\n        if (value != null) {\n          return renderOption(value as string);\n        }\n\n        return <span style={{ color: themeToken.colorTextPlaceholder }}>{props.placeholder}</span>;\n      }}\n      options={options}\n      optionLabelProp={void 0}\n      optionRender={(option) => renderOption(option.data.value as string)}\n      showSearch={{\n        filterOption: (inputValue, option) => matchSearchOption(inputValue, option!),\n      }}\n    />\n  );\n};\n\nexport default ACMEHttp01ProviderSelect;\n"
  },
  {
    "path": "ui/src/components/provider/AccessProviderPicker.tsx",
    "content": "﻿import { forwardRef, useImperativeHandle, useMemo, useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Avatar, Card, Empty, Input, type InputRef, Tag, Typography } from \"antd\";\n\nimport Show from \"@/components/Show\";\nimport { ACCESS_USAGES, type AccessProvider, type AccessUsageType, accessProvidersMap } from \"@/domain/provider\";\nimport { mergeCls } from \"@/utils/css\";\n\nimport { type SharedPickerProps, usePickerDataSource, usePickerWrapperCols } from \"./_shared\";\n\nexport interface AccessProviderPickerProps extends SharedPickerProps<AccessProvider> {\n  showOptionTags?: boolean | { [key in AccessUsageType | \"builtin\"]?: boolean };\n}\n\nexport interface AccessProviderPickerInstance {\n  inputRef: InputRef | null;\n}\n\nconst AccessProviderPicker = forwardRef<AccessProviderPickerInstance, AccessProviderPickerProps>(\n  ({ className, style, gap = \"medium\", placeholder, showOptionTags, showSearch = false, onFilter, onSelect }, ref) => {\n    const { t } = useTranslation();\n\n    const showOptionTagForDNS = useMemo(() => {\n      return typeof showOptionTags === \"object\" ? !!showOptionTags?.[ACCESS_USAGES.DNS] : !!showOptionTags;\n    }, [showOptionTags]);\n    const showOptionTagForHosting = useMemo(() => {\n      return typeof showOptionTags === \"object\" ? !!showOptionTags?.[ACCESS_USAGES.HOSTING] : !!showOptionTags;\n    }, [showOptionTags]);\n    const showOptionTagForCA = useMemo(() => {\n      return typeof showOptionTags === \"object\" ? !!showOptionTags?.[ACCESS_USAGES.CA] : !!showOptionTags;\n    }, [showOptionTags]);\n    const showOptionTagForNotification = useMemo(() => {\n      return typeof showOptionTags === \"object\" ? !!showOptionTags?.[ACCESS_USAGES.NOTIFICATION] : !!showOptionTags;\n    }, [showOptionTags]);\n    const showOptionTagForBuiltin = useMemo(() => {\n      return typeof showOptionTags === \"object\" ? !!showOptionTags?.[\"builtin\"] : !!showOptionTags;\n    }, [showOptionTags]);\n    const showOptionTagAnyhow = useMemo(() => {\n      return showOptionTagForDNS || showOptionTagForHosting || showOptionTagForCA || showOptionTagForNotification || showOptionTagForBuiltin;\n    }, [showOptionTagForDNS, showOptionTagForHosting, showOptionTagForCA, showOptionTagForNotification, showOptionTagForBuiltin]);\n\n    const { wrapperElRef, cols } = usePickerWrapperCols(showOptionTagAnyhow ? 240 : 200);\n\n    const [keyword, setKeyword] = useState<string>();\n    const keywordInputRef = useRef<InputRef>(null);\n\n    const dataSources = usePickerDataSource({\n      dataSource: Array.from(accessProvidersMap.values()),\n      filters: [onFilter!],\n      keyword: keyword,\n    });\n\n    const renderOption = (provider: AccessProvider) => {\n      return (\n        <div className=\"group/provider size-full\" key={provider.type}>\n          <Card\n            className={mergeCls(\"size-full overflow-hidden shadow\", provider.builtin ? \"cursor-not-allowed\" : void 0)}\n            styles={{\n              body: {\n                height: \"100%\",\n                padding: \"1.25rem 1rem\",\n              },\n            }}\n            hoverable\n            onClick={() => {\n              if (provider.builtin) {\n                return;\n              }\n\n              handleProviderTypeSelect(provider.type);\n            }}\n          >\n            <div className=\"flex size-full flex-col\">\n              <div className=\"flex flex-1 justify-between gap-3\">\n                <div className=\"flex-1\">\n                  <Typography.Text type={provider.builtin ? \"secondary\" : void 0}>{t(provider.name) || \"\\u00A0\"}</Typography.Text>\n                </div>\n                <div className=\"transition-all group-hover/provider:scale-110\">\n                  <Avatar className=\"bg-stone-50\" icon={<img src={provider.icon} />} shape=\"square\" size={28} />\n                </div>\n              </div>\n              <Show when={showOptionTagAnyhow}>\n                <div className=\"flex origin-left scale-80 items-center gap-1 whitespace-nowrap\">\n                  <Show when={showOptionTagForBuiltin && provider.builtin}>\n                    <Tag className=\"mt-4 -mb-2\" color=\"default\">\n                      {t(\"access.props.provider.builtin\")}\n                    </Tag>\n                  </Show>\n                  <Show when={showOptionTagForDNS && provider.usages.includes(ACCESS_USAGES.DNS)}>\n                    <Tag className=\"mt-4 -mb-2\" color=\"#d93f0b\">\n                      {t(\"access.props.provider.usage.dns\")}\n                    </Tag>\n                  </Show>\n                  <Show when={showOptionTagForHosting && provider.usages.includes(ACCESS_USAGES.HOSTING)}>\n                    <Tag className=\"mt-4 -mb-2\" color=\"#0052cc\">\n                      {t(\"access.props.provider.usage.hosting\")}\n                    </Tag>\n                  </Show>\n                  <Show when={showOptionTagForCA && provider.usages.includes(ACCESS_USAGES.CA)}>\n                    <Tag className=\"mt-4 -mb-2\" color=\"#0e8a16\">\n                      {t(\"access.props.provider.usage.ca\")}\n                    </Tag>\n                  </Show>\n                  <Show when={showOptionTagForNotification && provider.usages.includes(ACCESS_USAGES.NOTIFICATION)}>\n                    <Tag className=\"mt-4 -mb-2\" color=\"#1d76db\">\n                      {t(\"access.props.provider.usage.notification\")}\n                    </Tag>\n                  </Show>\n                </div>\n              </Show>\n            </div>\n          </Card>\n        </div>\n      );\n    };\n\n    const handleProviderTypeSelect = (value: string) => {\n      onSelect?.(value);\n    };\n\n    useImperativeHandle(ref, () => ({\n      get inputRef() {\n        return keywordInputRef.current;\n      },\n    }));\n\n    return (\n      <div className={className} style={style} ref={wrapperElRef}>\n        <Show when={showSearch}>\n          <div className=\"mb-4\">\n            <Input.Search ref={keywordInputRef} placeholder={placeholder ?? t(\"common.text.search\")} onChange={(e) => setKeyword(e.target.value.trim())} />\n          </div>\n        </Show>\n\n        <Show when={dataSources.filtered.length > 0} fallback={<Empty description={t(\"provider.text.nodata\")} image={Empty.PRESENTED_IMAGE_SIMPLE} />}>\n          <div\n            className={mergeCls(\"grid w-full gap-2\", `grid-cols-${cols}`, {\n              \"gap-4\": gap === \"large\",\n              \"gap-2\": gap === \"medium\",\n              \"gap-1\": gap === \"small\",\n              [`gap-${+gap || \"2\"}`]: typeof gap === \"number\",\n            })}\n          >\n            {dataSources.filtered.map((provider) => renderOption(provider))}\n          </div>\n        </Show>\n      </div>\n    );\n  }\n);\n\nexport default AccessProviderPicker;\n"
  },
  {
    "path": "ui/src/components/provider/AccessProviderSelect.tsx",
    "content": "import { useMemo } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Avatar, Select, Tag, Typography, theme } from \"antd\";\n\nimport Show from \"@/components/Show\";\nimport { ACCESS_USAGES, type AccessProvider, type AccessUsageType, accessProvidersMap } from \"@/domain/provider\";\nimport { matchSearchOption } from \"@/utils/search\";\n\nimport { type SharedSelectProps } from \"./_shared\";\n\nexport interface AccessProviderSelectProps extends SharedSelectProps<AccessProvider> {\n  showOptionTags?: boolean | { [key in AccessUsageType | \"builtin\"]?: boolean };\n}\n\nconst AccessProviderSelect = ({ showOptionTags, onFilter, ...props }: AccessProviderSelectProps = { showOptionTags: true }) => {\n  const { t } = useTranslation();\n\n  const { token: themeToken } = theme.useToken();\n\n  const showOptionTagForDNS = useMemo(() => {\n    return typeof showOptionTags === \"object\" ? !!showOptionTags?.[ACCESS_USAGES.DNS] : !!showOptionTags;\n  }, [showOptionTags]);\n  const showOptionTagForHosting = useMemo(() => {\n    return typeof showOptionTags === \"object\" ? !!showOptionTags?.[ACCESS_USAGES.HOSTING] : !!showOptionTags;\n  }, [showOptionTags]);\n  const showOptionTagForCA = useMemo(() => {\n    return typeof showOptionTags === \"object\" ? !!showOptionTags?.[ACCESS_USAGES.CA] : !!showOptionTags;\n  }, [showOptionTags]);\n  const showOptionTagForNotification = useMemo(() => {\n    return typeof showOptionTags === \"object\" ? !!showOptionTags?.[ACCESS_USAGES.NOTIFICATION] : !!showOptionTags;\n  }, [showOptionTags]);\n  const showOptionTagForBuiltin = useMemo(() => {\n    return typeof showOptionTags === \"object\" ? !!showOptionTags?.[\"builtin\"] : !!showOptionTags;\n  }, [showOptionTags]);\n\n  const options = useMemo<Array<{ key: string; value: string; label: string; data: AccessProvider }>>(() => {\n    return Array.from(accessProvidersMap.values())\n      .filter((provider) => {\n        if (onFilter) {\n          return onFilter(provider.type, provider);\n        }\n\n        return true;\n      })\n      .map((provider) => ({\n        key: provider.type,\n        value: provider.type,\n        label: t(provider.name),\n        disabled: provider.builtin,\n        data: provider,\n      }));\n  }, [onFilter]);\n\n  const renderOption = (key: string) => {\n    const provider = accessProvidersMap.get(key) ?? ({ type: \"\", name: \"\", icon: \"\", usages: [] } as unknown as AccessProvider);\n    return (\n      <div className=\"flex max-w-full items-center justify-between gap-4 overflow-hidden\">\n        <div className=\"flex items-center gap-2 truncate overflow-hidden\">\n          <Avatar shape=\"square\" src={provider.icon} size=\"small\" />\n          <Typography.Text className=\"flex-1 truncate overflow-hidden\" type={provider.builtin ? \"secondary\" : void 0} ellipsis>\n            {t(provider.name)}\n          </Typography.Text>\n        </div>\n        <div className=\"flex origin-right scale-80 items-center justify-center gap-1 whitespace-nowrap\">\n          <Show when={showOptionTagForBuiltin && provider.builtin}>\n            <Tag color=\"default\">{t(\"access.props.provider.builtin\")}</Tag>\n          </Show>\n          <Show when={showOptionTagForDNS && provider.usages.includes(ACCESS_USAGES.DNS)}>\n            <Tag color=\"#d93f0b\">{t(\"access.props.provider.usage.dns\")}</Tag>\n          </Show>\n          <Show when={showOptionTagForHosting && provider.usages.includes(ACCESS_USAGES.HOSTING)}>\n            <Tag color=\"#0052cc\">{t(\"access.props.provider.usage.hosting\")}</Tag>\n          </Show>\n          <Show when={showOptionTagForCA && provider.usages.includes(ACCESS_USAGES.CA)}>\n            <Tag color=\"#0e8a16\">{t(\"access.props.provider.usage.ca\")}</Tag>\n          </Show>\n          <Show when={showOptionTagForNotification && provider.usages.includes(ACCESS_USAGES.NOTIFICATION)}>\n            <Tag color=\"#1d76db\">{t(\"access.props.provider.usage.notification\")}</Tag>\n          </Show>\n        </div>\n      </div>\n    );\n  };\n\n  return (\n    <Select\n      {...props}\n      labelRender={({ value }) => {\n        if (value != null) {\n          return renderOption(value as string);\n        }\n\n        return <span style={{ color: themeToken.colorTextPlaceholder }}>{props.placeholder}</span>;\n      }}\n      options={options}\n      optionLabelProp={void 0}\n      optionRender={(option) => renderOption(option.data.value)}\n      showSearch={{\n        filterOption: (inputValue, option) => matchSearchOption(inputValue, option!),\n        optionFilterProp: \"label\",\n      }}\n    />\n  );\n};\n\nexport default AccessProviderSelect;\n"
  },
  {
    "path": "ui/src/components/provider/CAProviderSelect.tsx",
    "content": "import { useMemo } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useControllableValue, useMount } from \"ahooks\";\nimport { Avatar, Select, Typography, theme } from \"antd\";\n\nimport { type CAProvider, caProvidersMap } from \"@/domain/provider\";\nimport { useZustandShallowSelector } from \"@/hooks\";\nimport { useSSLProviderSettingsStore } from \"@/stores/settings\";\nimport { matchSearchOption } from \"@/utils/search\";\n\nimport { type SharedSelectProps, useSelectDataSource } from \"./_shared\";\n\nexport interface CAProviderSelectProps extends SharedSelectProps<CAProvider> {\n  showAvailability?: boolean;\n  showDefault?: boolean;\n}\n\nconst CAProviderSelect = ({ showAvailability, showDefault, onFilter, ...props }: CAProviderSelectProps) => {\n  const { t } = useTranslation();\n\n  const { token: themeToken } = theme.useToken();\n\n  const { settings: sslProviderSettings, loadSettings: loadSSLProviderSettings } = useSSLProviderSettingsStore(\n    useZustandShallowSelector([\"settings\", \"loadSettings\"])\n  );\n  useMount(() => loadSSLProviderSettings(false));\n\n  const [value, setValue] = useControllableValue<string | undefined>(props, {\n    valuePropName: \"value\",\n    defaultValuePropName: \"defaultValue\",\n    trigger: \"onChange\",\n  });\n\n  const defaultCAProvider = useMemo(() => {\n    return caProvidersMap.get(sslProviderSettings.provider);\n  }, [sslProviderSettings]);\n  const dataSources = useSelectDataSource({\n    dataSource: Array.from(caProvidersMap.values()),\n    filters: [onFilter!],\n  });\n  const options = useMemo(() => {\n    const convert = (providers: CAProvider[]): Array<{ key: string; value: string; label: string; data: CAProvider }> => {\n      return providers.map((provider) => ({\n        key: provider.type,\n        value: provider.type,\n        label: t(provider.name),\n        data: provider,\n      }));\n    };\n\n    const defaultOption = {\n      key: \"\",\n      value: \"\",\n      data: {} as CAProvider,\n    };\n    const plainOptions = convert(dataSources.filtered);\n    const groupOptions = [\n      {\n        label: t(\"provider.text.available_group\"),\n        options: convert(dataSources.available),\n      },\n      {\n        label: t(\"provider.text.unavailable_group\"),\n        options: convert(dataSources.unavailable),\n      },\n    ].filter((group) => group.options.length > 0);\n\n    return showAvailability\n      ? showDefault\n        ? [{ label: t(\"provider.text.default_group\"), options: [defaultOption] }, ...groupOptions]\n        : groupOptions\n      : showDefault\n        ? [defaultOption, ...plainOptions]\n        : plainOptions;\n  }, [showAvailability, showDefault, dataSources]);\n\n  const renderOption = (key: string) => {\n    if (key === \"\") {\n      return (\n        <div className=\"flex items-center justify-between gap-4\">\n          <div className=\"flex-1 truncate\">\n            <Typography.Text ellipsis>{showAvailability ? t(\"provider.text.default_ca_in_group\") : t(\"provider.text.default_ca\")}</Typography.Text>\n          </div>\n          {defaultCAProvider && (\n            <Typography.Text className=\"text-xs\" type=\"secondary\" ellipsis>\n              {t(defaultCAProvider.name)}\n            </Typography.Text>\n          )}\n        </div>\n      );\n    }\n\n    const provider = caProvidersMap.get(key);\n    return (\n      <div className=\"flex items-center gap-2 truncate overflow-hidden\">\n        <Avatar shape=\"square\" src={provider?.icon} size=\"small\" />\n        <Typography.Text ellipsis>{t(provider?.name ?? \"\")}</Typography.Text>\n      </div>\n    );\n  };\n\n  const handleChange = (value: string) => {\n    setValue((_) => (value !== \"\" ? value : void 0));\n  };\n\n  return (\n    <Select\n      {...props}\n      labelRender={({ value }) => {\n        if (value != null && value !== \"\") {\n          return renderOption(value as string);\n        }\n\n        return <span style={{ color: themeToken.colorTextPlaceholder }}>{props.placeholder}</span>;\n      }}\n      options={options}\n      optionLabelProp={void 0}\n      optionRender={(option) => renderOption(option.data.value as string)}\n      showSearch={{\n        filterOption: (inputValue, option) => {\n          if (option?.value === \"\") return true; // 始终显示系统默认项\n\n          return matchSearchOption(inputValue, option!);\n        },\n      }}\n      value={value}\n      onChange={handleChange}\n      onSelect={handleChange}\n    />\n  );\n};\n\nexport default CAProviderSelect;\n"
  },
  {
    "path": "ui/src/components/provider/DeploymentProviderPicker.tsx",
    "content": "﻿import { forwardRef, useImperativeHandle, useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Avatar, Card, Divider, Empty, Flex, Input, type InputRef, Tabs, Tooltip, Typography } from \"antd\";\n\nimport Show from \"@/components/Show\";\nimport { DEPLOYMENT_CATEGORIES, type DeploymentProvider, deploymentProvidersMap } from \"@/domain/provider\";\nimport { mergeCls } from \"@/utils/css\";\n\nimport { type SharedPickerProps, usePickerDataSource, usePickerWrapperCols } from \"./_shared\";\n\nexport interface DeploymentProviderPickerProps extends SharedPickerProps<DeploymentProvider> {\n  showAvailability?: boolean;\n}\n\nexport interface DeploymentProviderPickerInstance {\n  inputRef: InputRef | null;\n}\n\nconst DeploymentProviderPicker = forwardRef<DeploymentProviderPickerInstance, DeploymentProviderPickerProps>(\n  ({ className, style, gap = \"medium\", placeholder, showAvailability = false, showSearch = false, onFilter, onSelect }, ref) => {\n    const { t } = useTranslation();\n\n    const { wrapperElRef, cols } = usePickerWrapperCols(320);\n\n    const [category, setCategory] = useState<string>(DEPLOYMENT_CATEGORIES.ALL);\n\n    const [keyword, setKeyword] = useState<string>();\n    const keywordInputRef = useRef<InputRef>(null);\n\n    const dataSources = usePickerDataSource({\n      dataSource: Array.from(deploymentProvidersMap.values()),\n      filters: [\n        onFilter!,\n        (_, provider) => {\n          if (category && category !== DEPLOYMENT_CATEGORIES.ALL) {\n            return provider.category === category;\n          }\n\n          return true;\n        },\n      ],\n      keyword: keyword,\n      deps: [category],\n    });\n\n    const renderOption = (provider: DeploymentProvider, transparent: boolean = false) => {\n      return (\n        <div key={provider.type}>\n          <Card\n            className=\"group/provider h-16 w-full overflow-hidden shadow-sm\"\n            styles={{ body: { height: \"100%\", padding: \"0.5rem 1rem\" } }}\n            hoverable\n            onClick={() => {\n              handleProviderTypeSelect(provider.type);\n            }}\n          >\n            <div className={mergeCls(\"size-full\", transparent ? \"transition-opacity opacity-75 group-hover/provider:opacity-100\" : void 0)}>\n              <div className=\"flex size-full items-center gap-4 overflow-hidden\">\n                <div>\n                  <Avatar className=\"bg-stone-50\" icon={<img src={provider.icon} />} shape=\"square\" size={28} />\n                </div>\n                <div className=\"flex-1 overflow-hidden\">\n                  <div className=\"line-clamp-2 max-w-full\">\n                    <Tooltip title={t(provider.name)} mouseEnterDelay={1}>\n                      <Typography.Text>{t(provider.name) || \"\\u00A0\"}</Typography.Text>\n                    </Tooltip>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </Card>\n        </div>\n      );\n    };\n\n    const handleProviderTypeSelect = (value: string) => {\n      onSelect?.(value);\n    };\n\n    useImperativeHandle(ref, () => ({\n      get inputRef() {\n        return keywordInputRef.current;\n      },\n    }));\n\n    return (\n      <div className={className} style={style} ref={wrapperElRef}>\n        <Show when={showSearch}>\n          <div className=\"mb-4\">\n            <Input.Search ref={keywordInputRef} placeholder={placeholder ?? t(\"common.text.search\")} onChange={(e) => setKeyword(e.target.value.trim())} />\n          </div>\n        </Show>\n\n        <Flex>\n          <Tabs\n            defaultActiveKey={DEPLOYMENT_CATEGORIES.ALL}\n            items={[\n              DEPLOYMENT_CATEGORIES.ALL,\n              DEPLOYMENT_CATEGORIES.CDN,\n              DEPLOYMENT_CATEGORIES.STORAGE,\n              DEPLOYMENT_CATEGORIES.LOADBALANCE,\n              DEPLOYMENT_CATEGORIES.FIREWALL,\n              DEPLOYMENT_CATEGORIES.AV,\n              DEPLOYMENT_CATEGORIES.ACCELERATOR,\n              DEPLOYMENT_CATEGORIES.APIGATEWAY,\n              DEPLOYMENT_CATEGORIES.SERVERLESS,\n              DEPLOYMENT_CATEGORIES.WEBSITE,\n              DEPLOYMENT_CATEGORIES.SSL,\n              DEPLOYMENT_CATEGORIES.OTHER,\n            ].map((key) => ({\n              key: key,\n              label: t(`provider.category.${key}`),\n            }))}\n            size=\"small\"\n            tabBarStyle={{ marginLeft: \"-1rem\" }}\n            tabPlacement=\"start\"\n            onChange={(key) => setCategory(key)}\n          />\n\n          <div className=\"flex-1\">\n            <Show when={dataSources.filtered.length > 0} fallback={<Empty description={t(\"provider.text.nodata\")} image={Empty.PRESENTED_IMAGE_SIMPLE} />}>\n              <div\n                className={mergeCls(\"grid w-full gap-2\", `grid-cols-${cols}`, {\n                  \"gap-4\": gap === \"large\",\n                  \"gap-2\": gap === \"medium\",\n                  \"gap-1\": gap === \"small\",\n                  [`gap-${+gap || \"2\"}`]: typeof gap === \"number\",\n                })}\n              >\n                {(showAvailability ? dataSources.available : dataSources.filtered).map((provider) => renderOption(provider))}\n              </div>\n\n              <Show when={showAvailability && dataSources.unavailable.length > 0}>\n                <Divider size=\"small\">\n                  <Typography.Text className=\"text-xs font-normal\" type=\"secondary\">\n                    {t(\"provider.text.unavailable_divider\")}\n                  </Typography.Text>\n                </Divider>\n\n                <div\n                  className={mergeCls(\"grid w-full gap-2\", `grid-cols-${cols}`, {\n                    \"gap-4\": gap === \"large\",\n                    \"gap-2\": gap === \"medium\",\n                    \"gap-1\": gap === \"small\",\n                    [`gap-${+gap || \"2\"}`]: typeof gap === \"number\",\n                  })}\n                >\n                  {dataSources.unavailable.map((provider) => renderOption(provider, true))}\n                </div>\n              </Show>\n            </Show>\n          </div>\n        </Flex>\n      </div>\n    );\n  }\n);\n\nexport default DeploymentProviderPicker;\n"
  },
  {
    "path": "ui/src/components/provider/DeploymentProviderSelect.tsx",
    "content": "import { useMemo } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Avatar, Select, Typography, theme } from \"antd\";\n\nimport { type DeploymentProvider, deploymentProvidersMap } from \"@/domain/provider\";\nimport { matchSearchOption } from \"@/utils/search\";\n\nimport { type SharedSelectProps, useSelectDataSource } from \"./_shared\";\n\nexport interface DeploymentProviderSelectProps extends SharedSelectProps<DeploymentProvider> {\n  showAvailability?: boolean;\n}\n\nconst DeploymentProviderSelect = ({ showAvailability = false, onFilter, ...props }: DeploymentProviderSelectProps) => {\n  const { t } = useTranslation();\n\n  const { token: themeToken } = theme.useToken();\n\n  const dataSources = useSelectDataSource({\n    dataSource: Array.from(deploymentProvidersMap.values()),\n    filters: [onFilter!],\n  });\n  const options = useMemo(() => {\n    const convert = (providers: DeploymentProvider[]): Array<{ key: string; value: string; label: string; data: DeploymentProvider }> => {\n      return providers.map((provider) => ({\n        key: provider.type,\n        value: provider.type,\n        label: t(provider.name),\n        data: provider,\n      }));\n    };\n\n    return showAvailability\n      ? [\n          {\n            label: t(\"provider.text.available_group\"),\n            options: convert(dataSources.available),\n          },\n          {\n            label: t(\"provider.text.unavailable_group\"),\n            options: convert(dataSources.unavailable),\n          },\n        ].filter((group) => group.options.length > 0)\n      : convert(dataSources.filtered);\n  }, [showAvailability, dataSources]);\n\n  const renderOption = (key: string) => {\n    const provider = deploymentProvidersMap.get(key);\n    return (\n      <div className=\"flex items-center gap-2 truncate overflow-hidden\">\n        <Avatar shape=\"square\" src={provider?.icon} size=\"small\" />\n        <Typography.Text ellipsis>{t(provider?.name ?? \"\")}</Typography.Text>\n      </div>\n    );\n  };\n\n  return (\n    <Select\n      {...props}\n      labelRender={({ value }) => {\n        if (value != null) {\n          return renderOption(value as string);\n        }\n\n        return <span style={{ color: themeToken.colorTextPlaceholder }}>{props.placeholder}</span>;\n      }}\n      options={options}\n      optionLabelProp={void 0}\n      optionRender={(option) => renderOption(option.data.value as string)}\n      showSearch={{\n        filterOption: (inputValue, option) => matchSearchOption(inputValue, option!),\n      }}\n    />\n  );\n};\n\nexport default DeploymentProviderSelect;\n"
  },
  {
    "path": "ui/src/components/provider/NotificationProviderPicker.tsx",
    "content": "﻿import { forwardRef, useImperativeHandle, useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Avatar, Card, Divider, Empty, Input, type InputRef, Tooltip, Typography } from \"antd\";\n\nimport Show from \"@/components/Show\";\nimport { type NotificationProvider, notificationProvidersMap } from \"@/domain/provider\";\nimport { mergeCls } from \"@/utils/css\";\n\nimport { type SharedPickerProps, usePickerDataSource, usePickerWrapperCols } from \"./_shared\";\n\nexport interface NotificationProviderPickerProps extends SharedPickerProps<NotificationProvider> {\n  showAvailability?: boolean;\n}\n\nexport interface NotificationProviderPickerInstance {\n  inputRef: InputRef | null;\n}\n\nconst NotificationProviderPicker = forwardRef<NotificationProviderPickerInstance, NotificationProviderPickerProps>(\n  ({ className, style, gap = \"medium\", placeholder, showAvailability = false, showSearch = false, onFilter, onSelect }, ref) => {\n    const { t } = useTranslation();\n\n    const { wrapperElRef, cols } = usePickerWrapperCols(320);\n\n    const [keyword, setKeyword] = useState<string>();\n    const keywordInputRef = useRef<InputRef>(null);\n\n    const dataSources = usePickerDataSource({\n      dataSource: Array.from(notificationProvidersMap.values()),\n      filters: [onFilter!],\n      keyword: keyword,\n    });\n\n    const renderOption = (provider: NotificationProvider, transparent: boolean = false) => {\n      return (\n        <div key={provider.type}>\n          <Card\n            className=\"group/provider h-16 w-full overflow-hidden shadow-sm\"\n            styles={{ body: { height: \"100%\", padding: \"0.5rem 1rem\" } }}\n            hoverable\n            onClick={() => {\n              handleProviderTypeSelect(provider.type);\n            }}\n          >\n            <div className={mergeCls(\"size-full\", transparent ? \"transition-opacity opacity-75 group-hover/provider:opacity-100\" : void 0)}>\n              <div className=\"flex size-full items-center gap-4 overflow-hidden\">\n                <div>\n                  <Avatar className=\"bg-stone-50\" icon={<img src={provider.icon} />} shape=\"square\" size={28} />\n                </div>\n                <div className=\"flex-1 overflow-hidden\">\n                  <div className=\"line-clamp-2 max-w-full\">\n                    <Tooltip title={t(provider.name)} mouseEnterDelay={1}>\n                      <Typography.Text>{t(provider.name) || \"\\u00A0\"}</Typography.Text>\n                    </Tooltip>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </Card>\n        </div>\n      );\n    };\n\n    const handleProviderTypeSelect = (value: string) => {\n      onSelect?.(value);\n    };\n\n    useImperativeHandle(ref, () => ({\n      get inputRef() {\n        return keywordInputRef.current;\n      },\n    }));\n\n    return (\n      <div className={className} style={style} ref={wrapperElRef}>\n        <Show when={showSearch}>\n          <div className=\"mb-4\">\n            <Input.Search ref={keywordInputRef} placeholder={placeholder ?? t(\"common.text.search\")} onChange={(e) => setKeyword(e.target.value.trim())} />\n          </div>\n        </Show>\n\n        <Show when={dataSources.filtered.length > 0} fallback={<Empty description={t(\"provider.text.nodata\")} image={Empty.PRESENTED_IMAGE_SIMPLE} />}>\n          <div\n            className={mergeCls(\"grid w-full gap-2\", `grid-cols-${cols}`, {\n              \"gap-4\": gap === \"large\",\n              \"gap-2\": gap === \"medium\",\n              \"gap-1\": gap === \"small\",\n              [`gap-${+gap || \"2\"}`]: typeof gap === \"number\",\n            })}\n          >\n            {(showAvailability ? dataSources.available : dataSources.filtered).map((provider) => renderOption(provider))}\n          </div>\n\n          <Show when={showAvailability && dataSources.unavailable.length > 0}>\n            <Divider size=\"small\">\n              <Typography.Text className=\"text-xs font-normal\" type=\"secondary\">\n                {t(\"provider.text.unavailable_divider\")}\n              </Typography.Text>\n            </Divider>\n\n            <div\n              className={mergeCls(\"grid w-full gap-2\", `grid-cols-${cols}`, {\n                \"gap-4\": gap === \"large\",\n                \"gap-2\": gap === \"medium\",\n                \"gap-1\": gap === \"small\",\n                [`gap-${+gap || \"2\"}`]: typeof gap === \"number\",\n              })}\n            >\n              {dataSources.unavailable.map((provider) => renderOption(provider, true))}\n            </div>\n          </Show>\n        </Show>\n      </div>\n    );\n  }\n);\n\nexport default NotificationProviderPicker;\n"
  },
  {
    "path": "ui/src/components/provider/NotificationProviderSelect.tsx",
    "content": "import { useMemo } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Avatar, Select, Typography, theme } from \"antd\";\n\nimport { type NotificationProvider, notificationProvidersMap } from \"@/domain/provider\";\nimport { matchSearchOption } from \"@/utils/search\";\n\nimport { type SharedSelectProps, useSelectDataSource } from \"./_shared\";\n\nexport interface NotificationProviderSelectProps extends SharedSelectProps<NotificationProvider> {\n  showAvailability?: boolean;\n}\n\nconst NotificationProviderSelect = ({ showAvailability = false, onFilter, ...props }: NotificationProviderSelectProps) => {\n  const { t } = useTranslation();\n\n  const { token: themeToken } = theme.useToken();\n\n  const dataSources = useSelectDataSource({\n    dataSource: Array.from(notificationProvidersMap.values()),\n    filters: [onFilter!],\n  });\n  const options = useMemo(() => {\n    const convert = (providers: NotificationProvider[]): Array<{ key: string; value: string; label: string; data: NotificationProvider }> => {\n      return providers.map((provider) => ({\n        key: provider.type,\n        value: provider.type,\n        label: t(provider.name),\n        data: provider,\n      }));\n    };\n\n    return showAvailability\n      ? [\n          {\n            label: t(\"provider.text.available_group\"),\n            options: convert(dataSources.available),\n          },\n          {\n            label: t(\"provider.text.unavailable_group\"),\n            options: convert(dataSources.unavailable),\n          },\n        ].filter((group) => group.options.length > 0)\n      : convert(dataSources.filtered);\n  }, [showAvailability, dataSources]);\n\n  const renderOption = (key: string) => {\n    const provider = notificationProvidersMap.get(key);\n    return (\n      <div className=\"flex items-center gap-2 truncate overflow-hidden\">\n        <Avatar shape=\"square\" src={provider?.icon} size=\"small\" />\n        <Typography.Text ellipsis>{t(provider?.name ?? \"\")}</Typography.Text>\n      </div>\n    );\n  };\n\n  return (\n    <Select\n      {...props}\n      labelRender={({ value }) => {\n        if (value != null) {\n          return renderOption(value as string);\n        }\n\n        return <span style={{ color: themeToken.colorTextPlaceholder }}>{props.placeholder}</span>;\n      }}\n      options={options}\n      optionLabelProp={void 0}\n      optionRender={(option) => renderOption(option.data.value as string)}\n      showSearch={{\n        filterOption: (inputValue, option) => matchSearchOption(inputValue, option!),\n      }}\n    />\n  );\n};\n\nexport default NotificationProviderSelect;\n"
  },
  {
    "path": "ui/src/components/provider/_shared.ts",
    "content": "﻿import { useMemo, useRef } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useMount, useSize } from \"ahooks\";\n\nimport { type SelectProps } from \"antd\";\n\nimport { useZustandShallowSelector } from \"@/hooks\";\nimport { useAccessesStore } from \"@/stores/access\";\nimport { matchSearchString } from \"@/utils/search\";\n\ntype Provider = { type: string; name: string };\n\nexport interface SharedSelectProps<T extends Provider> extends Omit<SelectProps, \"labelRender\" | \"options\" | \"optionLabelProp\" | \"optionRender\"> {\n  className?: string;\n  style?: React.CSSProperties;\n  onFilter?: (value: string, option: T) => boolean;\n}\n\nexport const useSelectDataSource = <T extends Provider>({\n  dataSource,\n  filters,\n  deps = [],\n}: {\n  dataSource: T[];\n  filters?: Array<(value: string, option: T) => boolean>;\n  deps?: React.DependencyList;\n}) => {\n  const { accesses, fetchAccesses } = useAccessesStore(useZustandShallowSelector([\"accesses\", \"fetchAccesses\"]));\n  useMount(() => {\n    fetchAccesses(false);\n  });\n\n  const filteredDataSource = useMemo(() => {\n    return dataSource.filter((provider) => {\n      if (filters) {\n        for (const filter of filters) {\n          if (!filter) continue;\n          if (!filter(provider.type, provider)) return false;\n        }\n      }\n\n      return true;\n    });\n  }, [dataSource, filters, deps]);\n\n  const availableDataSource = useMemo(() => {\n    return filteredDataSource.filter((provider) => {\n      return accesses.some((access) => {\n        if (\"builtin\" in provider && provider.builtin) return true;\n        if (\"provider\" in provider) return access.provider === provider.provider;\n        return access.provider === provider.type;\n      });\n    });\n  }, [accesses, filteredDataSource, deps]);\n\n  const unavailableDataSource = useMemo(() => {\n    return filteredDataSource.filter((item) => !availableDataSource.includes(item));\n  }, [filteredDataSource, availableDataSource, deps]);\n\n  return {\n    raw: dataSource,\n    filtered: filteredDataSource,\n    available: availableDataSource,\n    unavailable: unavailableDataSource,\n  };\n};\n\nexport interface SharedPickerProps<T extends Provider> {\n  className?: string;\n  style?: React.CSSProperties;\n  gap?: number | \"small\" | \"medium\" | \"large\";\n  placeholder?: string;\n  showSearch?: boolean;\n  onFilter?: (value: string, option: T) => boolean;\n  onSelect?: (value: string) => void;\n}\n\nexport const usePickerWrapperCols = (width: number) => {\n  const wrapperElRef = useRef<HTMLDivElement>(null);\n  const wrapperSize = useSize(wrapperElRef);\n\n  const columns = useMemo(() => {\n    const wWidth = wrapperSize?.width ?? document.body.clientWidth - 256;\n    const wCols = Math.floor(wWidth / width);\n    return Math.min(9, Math.max(1, wCols));\n  }, [wrapperSize?.width, width]);\n\n  return {\n    wrapperElRef,\n    cols: columns,\n  };\n};\n\nexport const usePickerDataSource = <T extends Provider>({\n  dataSource,\n  filters,\n  keyword,\n  deps = [],\n}: {\n  dataSource: T[];\n  filters?: Array<(value: string, option: T) => boolean>;\n  keyword?: string;\n  deps?: React.DependencyList;\n}) => {\n  const { t } = useTranslation();\n\n  const { accesses, fetchAccesses } = useAccessesStore(useZustandShallowSelector([\"accesses\", \"fetchAccesses\"]));\n  useMount(() => {\n    fetchAccesses(false);\n  });\n\n  const filteredDataSource = useMemo(() => {\n    return dataSource\n      .filter((provider) => {\n        if (filters) {\n          for (const filter of filters) {\n            if (!filter) continue;\n            if (!filter(provider.type, provider)) return false;\n          }\n        }\n\n        return true;\n      })\n      .filter((provider) => {\n        if (keyword) {\n          return matchSearchString(keyword, provider.type) || matchSearchString(keyword, t(provider.name));\n        }\n\n        return true;\n      });\n  }, [dataSource, filters, keyword, deps]);\n\n  const availableDataSource = useMemo(() => {\n    return filteredDataSource.filter((provider) => {\n      return accesses.some((access) => {\n        if (\"builtin\" in provider && provider.builtin) return true;\n        if (\"provider\" in provider) return access.provider === provider.provider;\n        return access.provider === provider.type;\n      });\n    });\n  }, [accesses, filteredDataSource, deps]);\n\n  const unavailableDataSource = useMemo(() => {\n    return filteredDataSource.filter((item) => !availableDataSource.includes(item));\n  }, [filteredDataSource, availableDataSource, deps]);\n\n  return {\n    raw: dataSource,\n    filtered: filteredDataSource,\n    available: availableDataSource,\n    unavailable: unavailableDataSource,\n  };\n};\n"
  },
  {
    "path": "ui/src/components/workflow/WorkflowGraphExportBox.tsx",
    "content": "﻿import { useEffect, useState } from \"react\";\nimport { CopyToClipboard } from \"react-copy-to-clipboard\";\nimport { useTranslation } from \"react-i18next\";\nimport { FlowNodeBaseType, FlowNodeSplitType } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconClipboard } from \"@tabler/icons-react\";\nimport { App, Button, Form, Radio, Tooltip } from \"antd\";\nimport { stringify as stringifyYaml } from \"yaml\";\n\nimport CodeTextInput from \"@/components/CodeTextInput\";\nimport { type WorkflowGraph, type WorkflowNode } from \"@/domain/workflow\";\n\nimport { getAllNodeRegistries } from \"./designer/nodes\";\n\nexport type WorkflowGraphExportBoxFormats = \"json\" | \"yaml\";\n\nexport interface WorkflowGraphExportBoxProps {\n  className?: string;\n  style?: React.CSSProperties;\n  data: WorkflowGraph;\n}\n\nconst serialize = (graph: WorkflowGraph | undefined, format: WorkflowGraphExportBoxFormats): string | undefined => {\n  if (!graph) return;\n\n  const nodeRegistries = getAllNodeRegistries();\n\n  const deepConvert = (node: WorkflowNode): Map<string, unknown> => {\n    // 利用 Map 来保证字段序列化的有序性\n    const map = new Map<string, unknown>([\n      [\"id\", node.id],\n      [\"type\", node.type],\n      [\"name\", node.data.name],\n    ]);\n\n    if (node.data.disabled != null) {\n      if (node.data.disabled) {\n        map.set(\"disabled\", node.data.disabled);\n      }\n    }\n\n    if (node.data.config != null) {\n      map.set(\"config\", node.data.config);\n    }\n\n    if (node.blocks != null) {\n      const branchLikeNodeTypes = [\n        FlowNodeBaseType.BLOCK,\n        FlowNodeSplitType.SIMPLE_SPLIT,\n        FlowNodeSplitType.DYNAMIC_SPLIT,\n        FlowNodeSplitType.STATIC_SPLIT,\n      ] as string[];\n      const hasChildren = node.blocks.length > 0 || branchLikeNodeTypes.includes(nodeRegistries.find((r) => r.type === node.type)?.extend ?? \"\");\n      if (hasChildren) {\n        const children = node.blocks.map((block) => deepConvert(block));\n        map.set(\"blocks\", children);\n      }\n    }\n\n    Object.entries(node.data).forEach(([k, v]) => {\n      if (k === \"name\" || k === \"disabled\" || k === \"config\") return;\n      map.set(k, v);\n    });\n\n    return map;\n  };\n  const nodes = graph.nodes.map((node) => deepConvert(node));\n\n  let content: string = \"\";\n  switch (format) {\n    case \"json\":\n      content = JSON.stringify(\n        { ...graph, nodes },\n        (_, value) => {\n          if (value instanceof Map) {\n            return Object.fromEntries(value.entries());\n          } else {\n            return value;\n          }\n        },\n        2\n      );\n      break;\n\n    case \"yaml\":\n      content = stringifyYaml(\n        { ...graph, nodes },\n        {\n          indent: 2,\n          defaultKeyType: \"PLAIN\",\n          defaultStringType: \"QUOTE_DOUBLE\",\n        }\n      );\n      break;\n  }\n\n  return content;\n};\n\nconst WorkflowGraphExportBox = ({ className, style, data }: WorkflowGraphExportBoxProps) => {\n  const { t } = useTranslation();\n\n  const { message } = App.useApp();\n\n  const [format, setFormat] = useState<WorkflowGraphExportBoxFormats>(\"yaml\");\n  const [content, setContent] = useState<string>();\n\n  useEffect(() => {\n    setContent(serialize(data, format));\n  }, [data, format]);\n\n  return (\n    <Form className={className} style={style} layout=\"vertical\">\n      <Form.Item className=\"mb-4\" label={t(\"workflow.detail.design.action.export.form.format.label\")}>\n        <Radio.Group block value={format} onChange={(e) => setFormat(e.target.value)}>\n          <Radio.Button value=\"yaml\">YAML</Radio.Button>\n          <Radio.Button value=\"json\">JSON</Radio.Button>\n        </Radio.Group>\n      </Form.Item>\n\n      <Form.Item label={t(\"workflow.detail.design.action.export.form.content.label\")}>\n        <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n          <Tooltip title={t(\"common.button.copy\")}>\n            <CopyToClipboard\n              text={content!}\n              onCopy={() => {\n                message.success(t(\"common.text.copied\"));\n              }}\n            >\n              <Button icon={<IconClipboard size=\"1.25em\" />} disabled={!content} size=\"small\" type=\"text\" />\n            </CopyToClipboard>\n          </Tooltip>\n        </div>\n        <CodeTextInput height=\"calc(min(60vh, 512px))\" language={format} lineWrapping={false} value={content} readOnly />\n      </Form.Item>\n    </Form>\n  );\n};\n\nexport default WorkflowGraphExportBox;\n"
  },
  {
    "path": "ui/src/components/workflow/WorkflowGraphExportModal.tsx",
    "content": "﻿import { startTransition, useCallback, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useControllableValue } from \"ahooks\";\nimport { Modal } from \"antd\";\n\nimport { type WorkflowGraph } from \"@/domain/workflow\";\nimport { useTriggerElement } from \"@/hooks\";\n\nimport WorkflowGraphExportBox from \"./WorkflowGraphExportBox\";\n\nexport interface WorkflowGraphExportModalProps {\n  afterClose?: () => void;\n  data: WorkflowGraph;\n  loading?: boolean;\n  open?: boolean;\n  trigger?: React.ReactNode;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst WorkflowGraphExportModal = ({ afterClose, data, loading, trigger, ...props }: WorkflowGraphExportModalProps) => {\n  const { t } = useTranslation();\n\n  const [open, setOpen] = useControllableValue<boolean>(props, {\n    valuePropName: \"open\",\n    defaultValuePropName: \"defaultOpen\",\n    trigger: \"onOpenChange\",\n  });\n\n  const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) });\n\n  const handleCancelClick = () => {\n    setOpen(false);\n  };\n\n  return (\n    <>\n      {triggerEl}\n\n      <Modal\n        afterClose={afterClose}\n        closable\n        destroyOnHidden\n        footer={null}\n        loading={loading}\n        open={open}\n        title={t(\"workflow.detail.design.action.export.modal.title\")}\n        width=\"768px\"\n        onCancel={handleCancelClick}\n      >\n        <div className=\"py-3 pb-0\">\n          <WorkflowGraphExportBox data={data} />\n        </div>\n      </Modal>\n    </>\n  );\n};\n\nconst useModal = () => {\n  type DataType = WorkflowGraphExportModalProps[\"data\"];\n  const [data, setData] = useState<DataType>();\n  const [open, setOpen] = useState(false);\n\n  const onOpenChange = useCallback((open: boolean) => {\n    setOpen(open);\n  }, []);\n\n  return {\n    modalProps: {\n      afterClose: () => {\n        startTransition(() => {\n          if (!open) {\n            setData(void 0);\n          }\n        });\n      },\n      data: data!,\n      open,\n      onOpenChange,\n    },\n\n    open: ({ data }: { data: DataType }) => {\n      setData(data);\n      setOpen(true);\n    },\n    close: () => {\n      setOpen(false);\n    },\n  };\n};\n\nconst _default = Object.assign(WorkflowGraphExportModal, {\n  useModal,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/WorkflowGraphImportInputBox.tsx",
    "content": "﻿import { forwardRef, useImperativeHandle } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Form, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { parse as parseYaml } from \"yaml\";\nimport { z } from \"zod\";\n\nimport CodeTextInput from \"@/components/CodeTextInput\";\nimport { WORKFLOW_NODE_TYPES, type WorkflowGraph, type WorkflowNode, type WorkflowNodeType } from \"@/domain/workflow\";\nimport { useAntdForm } from \"@/hooks\";\n\nexport type WorkflowGraphImportInputBoxFormats = \"json\" | \"yaml\";\n\nexport interface WorkflowGraphImportInputBoxProps {\n  className?: string;\n  style?: React.CSSProperties;\n}\n\nexport interface WorkflowGraphImportInputBoxInstance {\n  validate: () => Promise<WorkflowGraph | undefined>;\n}\n\nconst deserialize = (content: string | undefined, format: WorkflowGraphImportInputBoxFormats): WorkflowGraph | undefined => {\n  if (!content?.trim()) return;\n\n  let temp: any;\n  switch (format) {\n    case \"json\":\n      temp = JSON.parse(content);\n      break;\n\n    case \"yaml\":\n      temp = parseYaml(content);\n      break;\n  }\n\n  const deepParse = (item: any): WorkflowNode => {\n    item = item ?? {};\n\n    const node: WorkflowNode = {\n      id: item.id?.toString() ?? \"\",\n      type: (item.type?.toString() ?? \"\") as WorkflowNodeType,\n      data: {\n        name: item.name?.toString() ?? \"\",\n        disabled: item.disabled === true || item.disabled === \"true\",\n        config: item.config,\n      },\n      blocks: Array.isArray(item.blocks) ? item.blocks.map((block: any) => deepParse(block)) : [],\n    };\n\n    if (item.data != null) {\n      Object.entries(item.data).forEach(([k, v]) => {\n        if (k === \"id\" || k === \"type\" || k === \"meta\" || k === \"blocks\") return;\n        if (k === \"name\" || k === \"disabled\" || k === \"config\") return;\n        node.data[k] = v;\n      });\n    }\n\n    return node;\n  };\n  const nodes = Array.from(temp.nodes ?? []).map((item) => deepParse(item));\n\n  return { nodes: nodes };\n};\n\nconst WorkflowGraphImportInputBox = forwardRef<WorkflowGraphImportInputBoxInstance, WorkflowGraphImportInputBoxProps>(({ className, style }, ref) => {\n  const { t } = useTranslation();\n\n  const formSchema = z\n    .object({\n      format: z.enum([\"json\", \"yaml\"]),\n      content: z.string().refine((v) => !!v?.trim(), t(\"workflow.detail.design.action.import.form.content.errmsg.invalid\")),\n    })\n    .superRefine((values, ctx) => {\n      let graph: WorkflowGraph | undefined;\n      try {\n        graph = deserialize(values.content, values.format);\n      } catch {\n        ctx.addIssue({\n          code: \"custom\",\n          message: t(\"workflow.detail.design.action.import.form.content.errmsg.invalid\"),\n          path: [\"content\"],\n        });\n        return;\n      }\n\n      if (graph) {\n        const errmsgs: string[] = [];\n\n        if (graph.nodes.at(0)?.type !== WORKFLOW_NODE_TYPES.START) {\n          errmsgs.push(t(\"workflow.detail.design.action.import.form.content.errmsg.first_node_start\"));\n        }\n\n        if (graph.nodes.at(-1)?.type !== WORKFLOW_NODE_TYPES.END) {\n          errmsgs.push(t(\"workflow.detail.design.action.import.form.content.errmsg.last_node_end\"));\n        }\n\n        let startNodeId: string | undefined;\n        let startNodeDuplicated: boolean = false;\n        const nodeIds = new Set<string>();\n        const deepValidate = (node: WorkflowNode) => {\n          // 验证字段：ID\n          if (!/^(?![_-])[a-zA-Z0-9_-]{1,32}$/.test(node.id)) {\n            errmsgs.push(t(\"workflow.detail.design.action.import.form.content.errmsg.invalid_id\", { nodeId: node.id }));\n          }\n\n          // 验证字段：配置项\n          if (node.data.config != null) {\n            if (typeof node.data.config !== \"object\" || Array.isArray(node.data.config)) {\n              errmsgs.push(t(\"workflow.detail.design.action.import.form.content.errmsg.invalid_config\", { nodeId: node.id }));\n            }\n          }\n\n          // 验证节点 ID 是否冲突\n          if (nodeIds.has(node.id)) {\n            errmsgs.push(t(\"workflow.detail.design.action.import.form.content.errmsg.conflict_id\", { nodeId: node.id }));\n          } else {\n            nodeIds.add(node.id);\n          }\n\n          // 验证开始节点是否重复\n          if (node.type === WORKFLOW_NODE_TYPES.START) {\n            if (startNodeId) {\n              if (!startNodeDuplicated) {\n                startNodeDuplicated = true;\n                errmsgs.push(t(\"workflow.detail.design.action.import.form.content.errmsg.duplicate_start\"));\n              }\n            } else {\n              startNodeId = node.id;\n            }\n          }\n\n          // 验证 Condition 分支结构\n          if (node.type === WORKFLOW_NODE_TYPES.CONDITION) {\n            const blocks = node.blocks ?? [];\n            const f1 = Array.isArray(blocks) && blocks.length > 0;\n            const f2 = Array.from(blocks).every((block) => block.type === WORKFLOW_NODE_TYPES.BRANCHBLOCK);\n            if (!f1 || !f2) {\n              errmsgs.push(t(\"workflow.detail.design.action.import.form.content.errmsg.abnormal_condition_branch\", { nodeId: node.id }));\n            }\n          } else if (node.type === WORKFLOW_NODE_TYPES.BRANCHBLOCK) {\n            const blocks = node.blocks ?? [];\n            const f1 = Array.isArray(blocks);\n            const f2 = Array.from(blocks).every((block) => block.type !== WORKFLOW_NODE_TYPES.BRANCHBLOCK);\n            if (!f1 || !f2) {\n              errmsgs.push(t(\"workflow.detail.design.action.import.form.content.errmsg.abnormal_condition_branch\", { nodeId: node.id }));\n            }\n          }\n\n          // 验证 TryCatch 分支结构\n          if (node.type === WORKFLOW_NODE_TYPES.TRYCATCH) {\n            const blocks = node.blocks ?? [];\n            const f1 = Array.isArray(blocks) && blocks.length >= 2;\n            const f2 = Array.from(blocks).at(0)?.type === WORKFLOW_NODE_TYPES.TRYBLOCK;\n            const f3 = Array.from(blocks).some((block) => block.type === WORKFLOW_NODE_TYPES.CATCHBLOCK);\n            if (!f1 || !f2 || !f3) {\n              errmsgs.push(t(\"workflow.detail.design.action.import.form.content.errmsg.abnormal_try_catch_branch\", { nodeId: node.id }));\n            }\n          } else if (node.type === WORKFLOW_NODE_TYPES.TRYBLOCK || node.type === WORKFLOW_NODE_TYPES.CATCHBLOCK) {\n            const blocks = node.blocks ?? [];\n            const f1 = Array.isArray(blocks);\n            const f2 = Array.from(blocks).every((block) => block.type !== WORKFLOW_NODE_TYPES.TRYBLOCK);\n            const f3 = Array.from(blocks).every((block) => block.type !== WORKFLOW_NODE_TYPES.CATCHBLOCK);\n            if (!f1 || !f2 || !f3) {\n              errmsgs.push(t(\"workflow.detail.design.action.import.form.content.errmsg.abnormal_try_catch_branch\", { nodeId: node.id }));\n            }\n          }\n\n          // 验证子节点\n          if (Array.isArray(node.blocks)) {\n            node.blocks.forEach((block) => deepValidate(block));\n          }\n        };\n        graph.nodes.forEach((node) => deepValidate(node));\n\n        const MAX_ISSUE_COUNT = 5;\n        for (let i = 0; i < Math.min(MAX_ISSUE_COUNT, errmsgs.length); i++) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: errmsgs[i],\n            path: [\"content\"],\n          });\n        }\n      }\n    });\n  const formRule = createSchemaFieldRule(formSchema);\n  const { form: formInst, formProps } = useAntdForm<z.infer<typeof formSchema>>({\n    name: \"workflowGraphExportInputBoxForm\",\n    initialValues: {\n      format: \"yaml\",\n      content: \"\",\n    },\n  });\n\n  const fieldFormat = Form.useWatch<WorkflowGraphImportInputBoxFormats>(\"format\", formInst);\n  const fieldContent = Form.useWatch<string>(\"content\", formInst);\n\n  const handleFormatChange = (format: WorkflowGraphImportInputBoxFormats) => {\n    formInst.setFieldValue(\"format\", format);\n\n    switch (format) {\n      case \"json\":\n        try {\n          JSON.parse(fieldContent);\n        } catch {\n          formInst.setFieldValue(\"content\", \"\");\n        }\n        break;\n\n      case \"yaml\":\n        try {\n          parseYaml(fieldContent);\n        } catch {\n          formInst.setFieldValue(\"content\", \"\");\n        }\n        break;\n    }\n  };\n\n  useImperativeHandle(ref, () => {\n    return {\n      validate: async () => {\n        const formValues = await formInst.validateFields();\n        return deserialize(formValues.content, formValues.format);\n      },\n    };\n  });\n\n  return (\n    <Form className={className} style={style} {...formProps} clearOnDestroy={true} form={formInst} layout=\"vertical\" preserve={false} scrollToFirstError>\n      <Form.Item className=\"mb-4\" name=\"format\" label={t(\"workflow.detail.design.action.import.form.format.label\")} rules={[formRule]}>\n        <Radio.Group block onChange={(e) => handleFormatChange(e.target.value)}>\n          <Radio.Button value=\"yaml\">YAML</Radio.Button>\n          <Radio.Button value=\"json\">JSON</Radio.Button>\n        </Radio.Group>\n      </Form.Item>\n\n      <Form.Item name=\"content\" label={t(\"workflow.detail.design.action.import.form.content.label\")} rules={[formRule]}>\n        <CodeTextInput height=\"calc(min(60vh, 512px))\" language={fieldFormat} lineWrapping={false} value={fieldContent} />\n      </Form.Item>\n    </Form>\n  );\n});\n\nexport default WorkflowGraphImportInputBox;\n"
  },
  {
    "path": "ui/src/components/workflow/WorkflowGraphImportModal.tsx",
    "content": "﻿import { startTransition, useCallback, useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useControllableValue } from \"ahooks\";\nimport { Button, Flex, Modal } from \"antd\";\n\nimport { type WorkflowGraph } from \"@/domain/workflow\";\nimport { useTriggerElement } from \"@/hooks\";\n\nimport WorkflowImportExportForm, { type WorkflowGraphImportInputBoxInstance } from \"./WorkflowGraphImportInputBox\";\n\nexport interface WorkflowGraphImportModalProps {\n  afterClose?: () => void;\n  open?: boolean;\n  trigger?: React.ReactNode;\n  onCancel?: () => void;\n  onOk?: (graph: WorkflowGraph) => void;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst WorkflowGraphImportModal = ({ afterClose, trigger, onCancel, onOk, ...props }: WorkflowGraphImportModalProps) => {\n  const { t } = useTranslation();\n\n  const [open, setOpen] = useControllableValue<boolean>(props, {\n    valuePropName: \"open\",\n    defaultValuePropName: \"defaultOpen\",\n    trigger: \"onOpenChange\",\n  });\n\n  const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) });\n\n  const graphInputBoxRef = useRef<WorkflowGraphImportInputBoxInstance>(null);\n\n  const handleCancelClick = () => {\n    setOpen(false);\n\n    onCancel?.();\n  };\n\n  const handleOkClick = async () => {\n    const graph = await graphInputBoxRef.current!.validate();\n\n    setOpen(false);\n\n    if (graph != null) {\n      onOk?.(graph);\n    }\n  };\n\n  return (\n    <>\n      {triggerEl}\n\n      <Modal\n        afterClose={afterClose}\n        closable\n        destroyOnHidden\n        footer={\n          <Flex className=\"px-2\" justify=\"end\" gap=\"small\">\n            <Button onClick={handleCancelClick}>{t(\"common.button.cancel\")}</Button>\n            <Button type=\"primary\" onClick={handleOkClick}>\n              {t(\"workflow.detail.design.action.import.modal.ok_button\")}\n            </Button>\n          </Flex>\n        }\n        open={open}\n        title={t(\"workflow.detail.design.action.import.modal.title\")}\n        width=\"768px\"\n        onCancel={handleCancelClick}\n      >\n        <div className=\"py-3\">\n          <WorkflowImportExportForm ref={graphInputBoxRef} />\n        </div>\n      </Modal>\n    </>\n  );\n};\n\nconst useModal = () => {\n  const [open, setOpen] = useState(false);\n  const [onOkHandler, setOnOkHandler] = useState<{ handler: WorkflowGraphImportModalProps[\"onOk\"] }>();\n\n  const onOpenChange = useCallback((open: boolean) => {\n    setOpen(open);\n  }, []);\n\n  return {\n    modalProps: {\n      afterClose: () => {\n        startTransition(() => {\n          if (!open) {\n            setOnOkHandler(void 0);\n          }\n        });\n      },\n      open,\n      onOk: (graph: WorkflowGraph) => {\n        onOkHandler?.handler?.(graph);\n      },\n      onOpenChange,\n    },\n\n    open: () => {\n      setOpen(true);\n\n      const { promise, resolve } = Promise.withResolvers<WorkflowGraph>();\n      setOnOkHandler({ handler: (graph) => resolve(graph) });\n      return promise;\n    },\n    close: () => {\n      setOpen(false);\n    },\n  };\n};\n\nconst _default = Object.assign(WorkflowGraphImportModal, {\n  useModal,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/WorkflowRunDetail.tsx",
    "content": "import { useEffect, useMemo, useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { EditorState, FlowLayoutDefault } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconBrowserShare, IconBug, IconCheck, IconDots, IconDownload, IconSettings2, IconTransferOut } from \"@tabler/icons-react\";\nimport { useRequest } from \"ahooks\";\nimport { Alert, App, Button, Card, Divider, Dropdown, Empty, Skeleton, Table, type TableProps, Tooltip, Typography, theme } from \"antd\";\nimport dayjs from \"dayjs\";\nimport { ClientResponseError } from \"pocketbase\";\n\nimport CertificateDetailDrawer from \"@/components/certificate/CertificateDetailDrawer\";\nimport Show from \"@/components/Show\";\nimport { type CertificateModel } from \"@/domain/certificate\";\nimport { WorkflowLogLevel, type WorkflowLogModel } from \"@/domain/workflowLog\";\nimport { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from \"@/domain/workflowRun\";\nimport { useBrowserTheme } from \"@/hooks\";\nimport { listByWorkflowRunId as listCertificatesByWorkflowRunId } from \"@/repository/certificate\";\nimport { listByWorkflowRunId as listLogsByWorkflowRunId } from \"@/repository/workflowLog\";\nimport { subscribe as subscribeWorkflowRun } from \"@/repository/workflowRun\";\nimport { mergeCls } from \"@/utils/css\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nimport WorkflowDesigner from \"./designer/Designer\";\nimport WorkflowToolbar from \"./designer/Toolbar\";\nimport WorkflowGraphExportModal from \"./WorkflowGraphExportModal\";\nimport WorkflowStatus from \"./WorkflowStatus\";\n\nexport interface WorkflowRunDetailProps {\n  className?: string;\n  style?: React.CSSProperties;\n  data: WorkflowRunModel;\n}\n\nconst WorkflowRunDetail = ({ className, style, ...props }: WorkflowRunDetailProps) => {\n  const { t } = useTranslation();\n\n  const [innerData, setInnerData] = useState(props.data);\n  const mergedData = useMemo(() => ({ ...props.data, ...innerData }), [innerData, props.data]);\n\n  const unsubscriberRef = useRef<() => void>();\n  useEffect(() => {\n    if (props.data.status === WORKFLOW_RUN_STATUSES.PENDING || props.data.status === WORKFLOW_RUN_STATUSES.PROCESSING) {\n      subscribeWorkflowRun(props.data.id, (cb) => {\n        setInnerData(cb.record);\n\n        if (cb.record.status !== WORKFLOW_RUN_STATUSES.PENDING && cb.record.status !== WORKFLOW_RUN_STATUSES.PROCESSING) {\n          unsubscriberRef.current?.();\n          unsubscriberRef.current = undefined;\n        }\n      }).then((unsubscriber) => {\n        unsubscriberRef.current = unsubscriber;\n      });\n    }\n\n    return () => {\n      unsubscriberRef.current?.();\n      unsubscriberRef.current = undefined;\n    };\n  }, [props.data.id, props.data.status]);\n\n  return (\n    <div className={className} style={style}>\n      <Alert\n        showIcon\n        title={\n          <div className=\"text-xs\">\n            {mergedData.endedAt\n              ? t(\"workflow_run.base.description_with_time_cost\", {\n                  trigger: t(`workflow_run.base.trigger.${mergedData.trigger}`),\n                  startedAt: dayjs(mergedData.startedAt).format(\"YYYY-MM-DD HH:mm:ss\"),\n                  timeCost: dayjs(mergedData.endedAt).diff(dayjs(mergedData.startedAt), \"second\") + \"s\",\n                })\n              : t(\"workflow_run.base.description\", {\n                  trigger: t(`workflow_run.base.trigger.${mergedData.trigger}`),\n                  startedAt: dayjs(mergedData.startedAt).format(\"YYYY-MM-DD HH:mm:ss\"),\n                })}\n          </div>\n        }\n        type={\n          {\n            [WORKFLOW_RUN_STATUSES.SUCCEEDED]: \"success\" as const,\n            [WORKFLOW_RUN_STATUSES.FAILED]: \"error\" as const,\n            [WORKFLOW_RUN_STATUSES.CANCELED]: \"warning\" as const,\n          }[mergedData.status] ?? (\"info\" as const)\n        }\n      />\n      {!!mergedData.error && (\n        <Alert\n          className=\"mt-1\"\n          icon={<IconBug size=\"1em\" color=\"var(--color-error)\" />}\n          showIcon\n          title={<div className=\"text-xs text-error\">{mergedData.error}</div>}\n        />\n      )}\n\n      <div className=\"mt-8\">\n        <Typography.Title level={5}>{t(\"workflow_run.process\")}</Typography.Title>\n        <WorkflowRunProcess runData={mergedData} />\n      </div>\n\n      <div className=\"mt-8\">\n        <Typography.Title level={5}>{t(\"workflow_run.logs\")}</Typography.Title>\n        <WorkflowRunLogs runData={mergedData} />\n      </div>\n\n      <Show when={mergedData.outputs && mergedData.outputs.length > 0}>\n        <div className=\"mt-8\">\n          <Typography.Title level={5}>{t(\"workflow_run.artifacts\")}</Typography.Title>\n          <WorkflowRunArtifacts runData={mergedData} />\n        </div>\n      </Show>\n    </div>\n  );\n};\n\nconst WorkflowRunProcess = ({ runData }: { runData: WorkflowRunModel }) => {\n  const { t } = useTranslation();\n\n  const { token: themeToken } = theme.useToken();\n\n  const { modalProps: graphExportModalProps, ...graphExportModal } = WorkflowGraphExportModal.useModal();\n\n  const handleExportClick = () => {\n    graphExportModal.open({ data: runData.graph! });\n  };\n\n  return (\n    <>\n      <Card\n        className=\"size-full overflow-hidden\"\n        styles={{\n          body: {\n            position: \"relative\",\n            height: \"240px\",\n            padding: 0,\n            cursor: \"grab\",\n          },\n        }}\n      >\n        <WorkflowDesigner\n          defaultEditorState={EditorState.STATE_MOUSE_FRIENDLY_SELECT.id}\n          defaultLayout={FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT}\n          initialData={runData.graph}\n          readonly\n        >\n          <div className=\"absolute bottom-4 z-10 w-full px-4\">\n            <div className=\"container\">\n              <div className=\"flex items-center justify-end gap-2\">\n                <WorkflowToolbar\n                  style={{\n                    backgroundColor: themeToken.colorBgContainer,\n                    borderRadius: themeToken.borderRadius,\n                  }}\n                  size=\"small\"\n                  showMouseState={false}\n                  showLayout={false}\n                  showMinimap={false}\n                  showZoomLevel={false}\n                />\n\n                <Dropdown\n                  menu={{\n                    items: [\n                      {\n                        key: \"export\",\n                        label: t(\"workflow_run.process.menu.export\"),\n                        icon: <IconTransferOut size=\"1.25em\" />,\n                        onClick: handleExportClick,\n                      },\n                    ],\n                  }}\n                  trigger={[\"click\"]}\n                >\n                  <Button icon={<IconDots size=\"1.25em\" />} size=\"small\" />\n                </Dropdown>\n              </div>\n            </div>\n          </div>\n        </WorkflowDesigner>\n      </Card>\n\n      <WorkflowGraphExportModal {...graphExportModalProps} />\n    </>\n  );\n};\n\nconst WorkflowRunLogs = ({ runData }: { runData: WorkflowRunModel }) => {\n  const { t } = useTranslation();\n\n  const { theme: browserTheme } = useBrowserTheme();\n\n  const { id: runId, status: runStatus } = runData;\n\n  type Log = Pick<WorkflowLogModel, \"timestamp\" | \"level\" | \"message\" | \"data\">;\n  type LogGroup = { id: string; name: string; records: Log[] };\n  const [listData, setListData] = useState<LogGroup[]>([]);\n  const { loading, ...req } = useRequest(\n    () => {\n      return listLogsByWorkflowRunId(runId);\n    },\n    {\n      refreshDeps: [runId, runStatus],\n      pollingInterval: 1000,\n      pollingWhenHidden: false,\n      throttleWait: 500,\n      onSuccess: (res) => {\n        if (res.items.length === listData.flatMap((e) => e.records).length) return;\n\n        setListData(\n          res.items.reduce((acc, e) => {\n            let group = acc.at(-1);\n            if (!group || group.id !== e.nodeId) {\n              group = { id: e.nodeId, name: e.nodeName, records: [] };\n              acc.push(group);\n            }\n            group.records.push({ timestamp: e.timestamp, level: e.level, message: e.message, data: e.data });\n            return acc;\n          }, [] as LogGroup[])\n        );\n      },\n      onFinally: () => {\n        if (runStatus !== WORKFLOW_RUN_STATUSES.PENDING && runStatus !== WORKFLOW_RUN_STATUSES.PROCESSING) {\n          req.cancel();\n        }\n      },\n      onError: (err) => {\n        if (err instanceof ClientResponseError && err.isAbort) {\n          return;\n        }\n\n        console.error(err);\n\n        throw err;\n      },\n    }\n  );\n\n  const [showTimestamp, setShowTimestamp] = useState(true);\n  const [showWhitespace, setShowWhitespace] = useState(true);\n\n  const renderLogRecord = (record: Log) => {\n    let timestamp = dayjs(record.timestamp).format(\"YYYY-MM-DD HH:mm:ss\");\n    timestamp = `[${timestamp}]`;\n\n    let message = <>{record.message}</>;\n    if (record.data != null && Object.keys(record.data).length > 0) {\n      message = (\n        <details>\n          <summary>{record.message}</summary>\n          {Object.entries(record.data).map(([key, value]) => (\n            <div key={key} className=\"flex space-x-2\" style={{ wordBreak: \"break-word\" }}>\n              <div className=\"whitespace-nowrap\">{key}:</div>\n              <div className={showWhitespace ? \"whitespace-normal\" : \"whitespace-pre-line\"}>{JSON.stringify(value)}</div>\n            </div>\n          ))}\n        </details>\n      );\n    }\n\n    return (\n      <div className=\"flex space-x-2\" style={{ wordBreak: \"break-word\" }}>\n        {showTimestamp && <div className=\"font-mono whitespace-nowrap text-stone-400\">{timestamp}</div>}\n        <div\n          className={mergeCls(\n            \"flex-1 font-mono\",\n            { [\"whitespace-pre-line\"]: !showWhitespace },\n            record.level < WorkflowLogLevel.Info\n              ? \"text-stone-400\"\n              : record.level < WorkflowLogLevel.Warn\n                ? \"\"\n                : record.level < WorkflowLogLevel.Error\n                  ? \"text-warning\"\n                  : \"text-error\"\n          )}\n        >\n          {message}\n        </div>\n      </div>\n    );\n  };\n\n  const handleDownloadClick = () => {\n    const NEWLINE = \"\\n\";\n    const logstr = listData\n      .map((group) => {\n        const escape = (str: string) => str.replaceAll(\"\\r\", \"\\\\r\").replaceAll(\"\\n\", \"\\\\n\");\n        return (\n          `#${group.id} ${group.name}` +\n          NEWLINE +\n          group.records\n            .map((record) => {\n              const datetime = dayjs(record.timestamp).format(\"YYYY-MM-DDTHH:mm:ss.SSSZ\");\n              const level =\n                record.level < WorkflowLogLevel.Info\n                  ? \"DBUG\"\n                  : record.level < WorkflowLogLevel.Warn\n                    ? \"INFO\"\n                    : record.level < WorkflowLogLevel.Error\n                      ? \"WARN\"\n                      : \"ERRO\";\n              const message = record.message;\n              const data = record.data && Object.keys(record.data).length > 0 ? JSON.stringify(record.data) : \"\";\n              return `[${datetime}] [${level}] ${escape(message)} ${escape(data)}`.trim();\n            })\n            .join(NEWLINE)\n        );\n      })\n      .join(NEWLINE + NEWLINE);\n    const blob = new Blob([logstr], { type: \"text/plain\" });\n    const url = URL.createObjectURL(blob);\n    const a = document.createElement(\"a\");\n    a.href = url;\n    a.download = `certimate_workflow_run_#${runId}_logs.txt`;\n    a.click();\n    URL.revokeObjectURL(url);\n    a.remove();\n  };\n\n  return (\n    <div className=\"rounded-md bg-black text-stone-200\">\n      <div className=\"flex items-center gap-2 p-4\">\n        <div className=\"grow overflow-hidden\">\n          <WorkflowStatus value={runStatus} />\n        </div>\n        <div>\n          <Dropdown\n            menu={{\n              items: [\n                {\n                  key: \"show-timestamp\",\n                  label: t(\"workflow_run.logs.menu.show_timestamps\"),\n                  icon: <IconCheck className={showTimestamp ? \"visible\" : \"invisible\"} size=\"1.25em\" />,\n                  onClick: () => setShowTimestamp(!showTimestamp),\n                },\n                {\n                  key: \"show-whitespace\",\n                  label: t(\"workflow_run.logs.menu.show_whitespaces\"),\n                  icon: <IconCheck className={showWhitespace ? \"visible\" : \"invisible\"} size=\"1.25em\" />,\n                  onClick: () => setShowWhitespace(!showWhitespace),\n                },\n                {\n                  type: \"divider\",\n                },\n                {\n                  key: \"download-logs\",\n                  label: t(\"workflow_run.logs.menu.download_logs\"),\n                  icon: <IconDownload className=\"invisible\" size=\"1.25em\" />,\n                  onClick: handleDownloadClick,\n                },\n              ],\n            }}\n            trigger={[\"click\"]}\n          >\n            <Button color=\"primary\" icon={<IconSettings2 size=\"1.25em\" />} ghost={browserTheme === \"light\"} />\n          </Dropdown>\n        </div>\n      </div>\n\n      <Divider className=\"my-0 bg-stone-800\" />\n\n      <div className=\"min-h-8 px-4 py-2\">\n        <Show when={!loading || listData.length > 0} fallback={<Skeleton />}>\n          {listData.map((group, index) => {\n            return (\n              <div key={index} className=\"mb-3\">\n                <div className=\"truncate text-xs/loose\">\n                  <span className=\"font-mono text-stone-400\">{`#${group.id}\\u00A0`}</span>\n                  <span>{group.name}</span>\n                </div>\n                <div className=\"flex flex-col text-xs/relaxed\">{group.records.map((record) => renderLogRecord(record))}</div>\n              </div>\n            );\n          })}\n        </Show>\n      </div>\n    </div>\n  );\n};\n\nconst WorkflowRunArtifacts = ({ runData }: { runData: WorkflowRunModel }) => {\n  const { t } = useTranslation();\n\n  const { notification } = App.useApp();\n\n  const { id: runId } = runData;\n\n  const tableColumns: TableProps<CertificateModel>[\"columns\"] = [\n    {\n      key: \"$index\",\n      align: \"center\",\n      fixed: \"left\",\n      width: 50,\n      render: (_, __, index) => index + 1,\n    },\n    {\n      key: \"type\",\n      title: t(\"workflow_run_artifact.props.type\"),\n      render: () => t(\"workflow_run_artifact.props.type.certificate\"),\n    },\n    {\n      key: \"name\",\n      title: t(\"workflow_run_artifact.props.name\"),\n      render: (_, record) => {\n        return (\n          <div className=\"max-w-full truncate\">\n            <Typography.Text delete={!!record.deleted} ellipsis>\n              {record.subjectAltNames}\n            </Typography.Text>\n          </div>\n        );\n      },\n    },\n    {\n      key: \"$action\",\n      align: \"end\",\n      width: 32,\n      render: (_, record) => (\n        <div className=\"flex items-center justify-end\">\n          <CertificateDetailDrawer\n            data={record}\n            trigger={\n              <Tooltip title={t(\"common.button.view\")}>\n                <Button color=\"primary\" disabled={!!record.deleted} icon={<IconBrowserShare size=\"1.25em\" />} variant=\"text\" />\n              </Tooltip>\n            }\n          />\n        </div>\n      ),\n    },\n  ];\n  const [tableData, setTableData] = useState<CertificateModel[]>([]);\n  const { loading } = useRequest(\n    () => {\n      // TODO: 目前输出产物只有证书\n      return listCertificatesByWorkflowRunId(runId);\n    },\n    {\n      refreshDeps: [runId],\n      onSuccess: (res) => {\n        setTableData(res.items);\n      },\n      onError: (err) => {\n        if (err instanceof ClientResponseError && err.isAbort) {\n          return;\n        }\n\n        console.error(err);\n        notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n\n        throw err;\n      },\n    }\n  );\n\n  return (\n    <Table<CertificateModel>\n      columns={tableColumns}\n      dataSource={tableData}\n      loading={loading}\n      locale={{\n        emptyText: <Empty description={t(\"common.text.nodata\")} image={Empty.PRESENTED_IMAGE_SIMPLE} />,\n      }}\n      pagination={false}\n      rowKey={(record) => record.id}\n      size=\"small\"\n    />\n  );\n};\n\nexport default WorkflowRunDetail;\n"
  },
  {
    "path": "ui/src/components/workflow/WorkflowRunDetailDrawer.tsx",
    "content": "import { startTransition, useCallback, useState } from \"react\";\nimport { IconX } from \"@tabler/icons-react\";\nimport { useControllableValue, useGetState } from \"ahooks\";\nimport { Button, Drawer, Flex } from \"antd\";\n\nimport Show from \"@/components/Show\";\nimport { type WorkflowRunModel } from \"@/domain/workflowRun\";\nimport { useTriggerElement } from \"@/hooks\";\n\nimport WorkflowRunDetail from \"./WorkflowRunDetail\";\n\nexport interface WorkflowRunDetailDrawerProps {\n  afterClose?: () => void;\n  data?: WorkflowRunModel;\n  loading?: boolean;\n  open?: boolean;\n  trigger?: React.ReactNode;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst WorkflowRunDetailDrawer = ({ afterClose, data, loading, trigger, ...props }: WorkflowRunDetailDrawerProps) => {\n  const [open, setOpen] = useControllableValue<boolean>(props, {\n    valuePropName: \"open\",\n    defaultValuePropName: \"defaultOpen\",\n    trigger: \"onOpenChange\",\n  });\n\n  const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) });\n\n  return (\n    <>\n      {triggerEl}\n\n      <Drawer\n        afterOpenChange={(open) => !open && afterClose?.()}\n        closeIcon={false}\n        destroyOnHidden\n        open={open}\n        loading={loading}\n        placement=\"right\"\n        size=\"large\"\n        title={\n          <Flex align=\"center\" justify=\"space-between\" gap=\"small\">\n            <div className=\"flex-1 truncate\">{data ? `Workflow Run #${data.id}` : \"Workflow Run\"}</div>\n            <Button\n              className=\"ant-drawer-close\"\n              style={{ marginInline: 0 }}\n              icon={<IconX size=\"1.25em\" />}\n              size=\"small\"\n              type=\"text\"\n              onClick={() => setOpen(false)}\n            />\n          </Flex>\n        }\n        onClose={() => setOpen(false)}\n      >\n        <Show when={!!data}>\n          <WorkflowRunDetail data={data!} />\n        </Show>\n      </Drawer>\n    </>\n  );\n};\n\nconst useDrawer = () => {\n  type DataType = WorkflowRunDetailDrawerProps[\"data\"];\n  const [data, setData, getData] = useGetState<DataType>();\n  const [loading, setLoading] = useState<boolean>();\n  const [open, setOpen] = useState(false);\n\n  const onOpenChange = useCallback((open: boolean) => {\n    setOpen(open);\n  }, []);\n\n  return {\n    drawerProps: {\n      afterClose: () => {\n        startTransition(() => {\n          if (!open) {\n            setData(void 0);\n            setLoading(void 0);\n          }\n        });\n      },\n      data,\n      loading,\n      open,\n      onOpenChange,\n    },\n\n    open: ({ data, loading }: { data: NonNullable<DataType>; loading?: boolean }) => {\n      setData(data);\n      setLoading(loading);\n      setOpen(true);\n\n      return {\n        safeUpdate: ({ data, loading }: { data?: NonNullable<DataType>; loading?: boolean }) => {\n          if (data != null) {\n            if (data.id !== getData()?.id) return; // 确保数据不脏读\n\n            setData(data);\n          }\n\n          if (loading != null) {\n            setLoading(loading);\n          }\n        },\n      };\n    },\n    close: () => {\n      setOpen(false);\n    },\n  };\n};\n\nconst _default = Object.assign(WorkflowRunDetailDrawer, {\n  useDrawer,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/WorkflowStatus.tsx",
    "content": "﻿import { useTranslation } from \"react-i18next\";\nimport {\n  IconCircleCheck,\n  IconCircleCheckFilled,\n  IconCircleDashed,\n  IconCircleOff,\n  IconCircleX,\n  IconCircleXFilled,\n  IconClock,\n  IconClockFilled,\n  IconLoader3,\n} from \"@tabler/icons-react\";\nimport { Typography, theme } from \"antd\";\n\nimport { WORKFLOW_RUN_STATUSES, type WorkflorRunStatusType } from \"@/domain/workflowRun\";\nimport { mergeCls } from \"@/utils/css\";\n\nconst useColor = (value: WorkflorRunStatusType | string, defaultColor?: string | false) => {\n  const { token: themeToken } = theme.useToken();\n\n  switch (value) {\n    case WORKFLOW_RUN_STATUSES.PENDING:\n      if (defaultColor == null || !defaultColor) {\n        return themeToken.colorTextSecondary;\n      }\n      break;\n    case WORKFLOW_RUN_STATUSES.PROCESSING:\n      if (defaultColor == null || !defaultColor) {\n        return themeToken.colorInfo;\n      }\n      break;\n    case WORKFLOW_RUN_STATUSES.SUCCEEDED:\n      if (defaultColor == null || !defaultColor) {\n        return themeToken.colorSuccess;\n      }\n      break;\n    case WORKFLOW_RUN_STATUSES.FAILED:\n      if (defaultColor == null || !defaultColor) {\n        return themeToken.colorError;\n      }\n      break;\n    case WORKFLOW_RUN_STATUSES.CANCELED:\n      if (defaultColor == null || !defaultColor) {\n        return themeToken.colorWarning;\n      }\n      break;\n    default:\n      if (defaultColor == null || !defaultColor) {\n        return themeToken.colorTextSecondary;\n      }\n      break;\n  }\n\n  return defaultColor;\n};\n\nexport interface WorkflowStatusIconProps {\n  className?: string;\n  style?: React.CSSProperties;\n  color?: string | false;\n  size?: number | string;\n  type?: \"filled\" | \"outlined\";\n  value: WorkflorRunStatusType | string;\n}\n\nconst WorkflowStatusIcon = ({ className, style, size = \"1.25em\", type = \"outlined\", value, ...props }: WorkflowStatusIconProps) => {\n  const color = useColor(value, props.color);\n\n  switch (value) {\n    case WORKFLOW_RUN_STATUSES.PENDING:\n      return (\n        <span className={mergeCls(\"anticon\", className)} style={style} role=\"img\">\n          {type === \"filled\" ? <IconClockFilled color={color} size={size} /> : <IconClock color={color} size={size} />}\n        </span>\n      );\n    case WORKFLOW_RUN_STATUSES.PROCESSING:\n      return (\n        <span className={mergeCls(\"anticon\", \"animate-spin\", className)} style={style} role=\"img\">\n          <IconLoader3 color={color} size={size} />\n        </span>\n      );\n    case WORKFLOW_RUN_STATUSES.SUCCEEDED:\n      return (\n        <span className={mergeCls(\"anticon\", className)} style={style} role=\"img\">\n          {type === \"filled\" ? <IconCircleCheckFilled color={color} size={size} /> : <IconCircleCheck color={color} size={size} />}\n        </span>\n      );\n    case WORKFLOW_RUN_STATUSES.FAILED:\n      return (\n        <span className={mergeCls(\"anticon\", className)} style={style} role=\"img\">\n          {type === \"filled\" ? <IconCircleXFilled color={color} size={size} /> : <IconCircleX color={color} size={size} />}\n        </span>\n      );\n    case WORKFLOW_RUN_STATUSES.CANCELED:\n      return (\n        <span className={mergeCls(\"anticon\", className)} style={style} role=\"img\">\n          <IconCircleOff color={color} size={size} />\n        </span>\n      );\n    default:\n      return (\n        <span className={mergeCls(\"anticon\", className)} style={style} role=\"img\">\n          <IconCircleDashed color={color} size={size} />\n        </span>\n      );\n  }\n};\n\nexport interface WorkflowStatusProps {\n  className?: string;\n  style?: React.CSSProperties;\n  children?: React.ReactNode;\n  color?: string | false;\n  showIcon?: boolean;\n  type?: WorkflowStatusIconProps[\"type\"];\n  value: WorkflorRunStatusType | string;\n}\n\nconst WorkflowStatus = ({ className, style, children, showIcon = true, type, value, ...props }: WorkflowStatusProps) => {\n  const { t } = useTranslation();\n\n  const color = useColor(value, props.color);\n\n  const renderIcon = () => (showIcon ? <WorkflowStatusIcon type={type} value={value} /> : null);\n\n  switch (value) {\n    case WORKFLOW_RUN_STATUSES.PENDING:\n    case WORKFLOW_RUN_STATUSES.PROCESSING:\n    case WORKFLOW_RUN_STATUSES.SUCCEEDED:\n    case WORKFLOW_RUN_STATUSES.FAILED:\n    case WORKFLOW_RUN_STATUSES.CANCELED:\n      return (\n        <Typography.Text className={className} style={style}>\n          <div className=\"flex items-center gap-2\">\n            {renderIcon()}\n            {children != null ? children : <span style={{ color: color }}>{t(`workflow_run.props.status.${value.toLowerCase()}`)}</span>}\n          </div>\n        </Typography.Text>\n      );\n    default:\n      return (\n        <Typography.Text className={className} style={style}>\n          <div className=\"flex items-center gap-2\">{children != null ? children : <></>}</div>\n        </Typography.Text>\n      );\n  }\n};\n\nconst _default = Object.assign(WorkflowStatus, {\n  Icon: WorkflowStatusIcon,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/Designer.tsx",
    "content": "import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from \"react\";\nimport {\n  ConstantKeys,\n  EditorRenderer,\n  EditorState,\n  FixedLayoutEditorProvider,\n  type FixedLayoutPluginContext,\n  type FixedLayoutProps,\n  type FlowDocumentJSON,\n  FlowLayoutDefault,\n  type FlowNodeEntity,\n  FlowTextKey,\n} from \"@flowgram.ai/fixed-layout-editor\";\nimport { createMinimapPlugin } from \"@flowgram.ai/minimap-plugin\";\nimport \"@flowgram.ai/fixed-layout-editor/index.css\";\nimport { theme } from \"antd\";\n\nimport { DegisnerContextProvider } from \"./_context\";\nimport { getAllElements } from \"./elements\";\nimport NodeRender from \"./NodeRender\";\nimport { getAllNodeRegistries } from \"./nodes\";\nimport { BranchNode } from \"./nodes/_shared\";\nimport \"./flowgram.css\";\n\nexport interface DesignerProps {\n  className?: string;\n  style?: React.CSSProperties;\n  children?: React.ReactNode;\n  defaultEditorState?: string;\n  defaultLayout?: string;\n  initialData?: FlowDocumentJSON;\n  readonly?: boolean;\n  onDocumentChange?: (ctx: FixedLayoutPluginContext) => void;\n  onNodeChange?: (ctx: FixedLayoutPluginContext, node: FlowNodeEntity) => void;\n  onNodeClick?: (ctx: FixedLayoutPluginContext, node: FlowNodeEntity) => void;\n}\n\nexport interface DesignerInstance extends FixedLayoutPluginContext {\n  validateNode(node: string | FlowNodeEntity): Promise<boolean>;\n  validateAllNodes(): Promise<boolean>;\n}\n\nconst Designer = forwardRef<DesignerInstance, DesignerProps>(\n  ({ className, style, children, defaultEditorState, defaultLayout, initialData, readonly, onDocumentChange, onNodeChange, onNodeClick }, ref) => {\n    const { token: themeToken } = theme.useToken();\n\n    const rendered = useRef(false);\n\n    const flowgramEditorRef = useRef<FixedLayoutPluginContext>(null);\n    const flowgramEditorProps = useMemo<FixedLayoutProps>(\n      () => ({\n        defaultLayout: defaultLayout,\n\n        initialData: initialData,\n\n        constants: {\n          [ConstantKeys.BASE_COLOR]: themeToken.colorBorder,\n          [ConstantKeys.BASE_ACTIVATED_COLOR]: themeToken.colorPrimary,\n          [ConstantKeys.NODE_SPACING]: 48,\n          [ConstantKeys.BRANCH_SPACING]: 48,\n        },\n\n        background: {\n          backgroundColor: themeToken.colorBgContainer,\n          dotSize: 0,\n        },\n\n        playground: {\n          autoFocus: true,\n          autoResize: true,\n          preventGlobalGesture: true,\n        },\n\n        selectBox: {\n          enable: false,\n        },\n\n        scroll: {\n          enableScrollLimit: true,\n        },\n\n        readonly: readonly,\n\n        nodeEngine: {\n          enable: true,\n        },\n\n        variableEngine: {\n          enable: true,\n        },\n\n        materials: {\n          components: getAllElements(),\n          renderTexts: {\n            [FlowTextKey.TRY_START_TEXT]: \"Try\",\n            [FlowTextKey.TRY_END_TEXT]: \"Then\",\n            [FlowTextKey.CATCH_TEXT]: \"Catch\",\n          },\n          renderDefaultNode: NodeRender,\n        },\n\n        nodeRegistries: getAllNodeRegistries(),\n\n        getNodeDefaultRegistry(type) {\n          return {\n            type,\n            meta: {\n              defaultExpanded: true,\n            },\n            formMeta: {\n              render: () => <BranchNode description={type} />,\n            },\n          };\n        },\n\n        plugins: () => [\n          createMinimapPlugin({\n            disableLayer: true,\n            enableDisplayAllNodes: true,\n            canvasStyle: {\n              canvasWidth: 160,\n              canvasHeight: 160,\n            },\n          }),\n        ],\n\n        onInit: (ctx) => {\n          if (defaultEditorState != null) {\n            ctx.playground.editorState.changeState(defaultEditorState);\n          } else {\n            const maybeMobile = [\"android\", \"ios\", \"iphone\", \"ipad\", \"micromessenger\"].some((s) => navigator.userAgent.includes(s));\n            if (maybeMobile) {\n              ctx.playground.editorState.changeState(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id);\n            }\n          }\n        },\n\n        onAllLayersRendered: (ctx) => {\n          rendered.current = true;\n\n          // 画布初始化后向下滚动一点，露出可能被 Alert 遮挡的部分\n          if (defaultLayout === FlowLayoutDefault.VERTICAL_FIXED_LAYOUT) {\n            setTimeout(() => {\n              ctx.playground.config.scroll({ scrollY: -80 });\n            }, 1);\n          }\n        },\n      }),\n      [defaultEditorState, defaultLayout, initialData, readonly, onDocumentChange, themeToken]\n    );\n\n    useEffect(() => {\n      const d = flowgramEditorRef.current!.document.originTree.onTreeChange(() => {\n        if (rendered.current) {\n          onDocumentChange?.(flowgramEditorRef.current!);\n        }\n      });\n\n      return () => d.dispose();\n    }, [onDocumentChange]);\n\n    useImperativeHandle(ref, () => {\n      return {\n        get clipboard() {\n          return flowgramEditorRef.current!.clipboard;\n        },\n        get container() {\n          return flowgramEditorRef.current!.container;\n        },\n        get document() {\n          return flowgramEditorRef.current!.document;\n        },\n        get history() {\n          return flowgramEditorRef.current!.history;\n        },\n        get operation() {\n          return flowgramEditorRef.current!.operation;\n        },\n        get playground() {\n          return flowgramEditorRef.current!.playground;\n        },\n        get selection() {\n          return flowgramEditorRef.current!.selection;\n        },\n        get tools() {\n          return flowgramEditorRef.current!.tools;\n        },\n\n        get(identifier) {\n          return flowgramEditorRef.current!.get(identifier);\n        },\n        getAll(identifier) {\n          return flowgramEditorRef.current!.getAll(identifier);\n        },\n        validateNode(node) {\n          if (typeof node === \"string\") {\n            node = flowgramEditorRef.current!.document.getNode(node)!;\n          }\n\n          const form = node.form;\n          return form ? form.validate().then((res) => res && !form.state.invalid) : Promise.resolve(true);\n        },\n        validateAllNodes() {\n          const nodes = flowgramEditorRef.current!.document.getAllNodes();\n          const forms = nodes.map((node) => node.form).filter((form) => form != null);\n          return Promise.allSettled(forms.map((form) => form.validate())).then((res) => forms.every((form, index) => res[index] && !form.state.invalid));\n        },\n      };\n    });\n\n    return (\n      <FixedLayoutEditorProvider ref={flowgramEditorRef} {...flowgramEditorProps}>\n        <DegisnerContextProvider\n          value={{\n            onDocumentChange: () => onDocumentChange?.(flowgramEditorRef.current!),\n            onNodeChange: (node) => onNodeChange?.(flowgramEditorRef.current!, node),\n            onNodeClick: (node) => onNodeClick?.(flowgramEditorRef.current!, node),\n          }}\n        >\n          <EditorRenderer className={className} style={style} />\n          {children}\n        </DegisnerContextProvider>\n      </FixedLayoutEditorProvider>\n    );\n  }\n);\n\nexport default Designer;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/Minimap.tsx",
    "content": "import { useService } from \"@flowgram.ai/fixed-layout-editor\";\nimport { FlowMinimapService, MinimapRender } from \"@flowgram.ai/minimap-plugin\";\n\nexport interface MinimapProps {\n  className?: string;\n  style?: React.CSSProperties;\n}\n\nconst Minimap = ({ className, style }: MinimapProps) => {\n  const minimapService = useService(FlowMinimapService);\n\n  return (\n    <div className={className} style={style}>\n      <MinimapRender\n        service={minimapService}\n        panelStyles={{}}\n        containerStyles={{\n          pointerEvents: \"auto\",\n          position: \"relative\",\n          top: \"unset\",\n          right: \"unset\",\n          bottom: \"unset\",\n          left: \"unset\",\n        }}\n        inactiveStyle={{\n          opacity: 1,\n          scale: 0.5,\n          translateX: 0,\n          translateY: 0,\n        }}\n      />\n    </div>\n  );\n};\n\nexport default Minimap;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/NodeDrawer.tsx",
    "content": "import { startTransition, useCallback, useMemo, useState } from \"react\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { useControllableValue } from \"ahooks\";\n\nimport Show from \"@/components/Show\";\nimport { useTriggerElement } from \"@/hooks\";\n\nimport BizApplyNodeConfigDrawer from \"./forms/BizApplyNodeConfigDrawer\";\nimport BizDeployNodeConfigDrawer from \"./forms/BizDeployNodeConfigDrawer\";\nimport BizMonitorNodeConfigDrawer from \"./forms/BizMonitorNodeConfigDrawer\";\nimport BizNotifyNodeConfigDrawer from \"./forms/BizNotifyNodeConfigDrawer\";\nimport BizUploadNodeConfigDrawer from \"./forms/BizUploadNodeConfigDrawer\";\nimport BranchBlockNodeConfigDrawer from \"./forms/BranchBlockNodeConfigDrawer\";\nimport DelayNodeConfigDrawer from \"./forms/DelayNodeConfigDrawer\";\nimport StartNodeConfigDrawer from \"./forms/StartNodeConfigDrawer\";\nimport { NodeType } from \"./nodes/typings\";\n\nexport interface NodeDrawerProps {\n  afterClose?: () => void;\n  children?: React.ReactNode;\n  loading?: boolean;\n  node?: FlowNodeEntity;\n  open?: boolean;\n  trigger?: React.ReactNode;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst NodeDrawer = ({ node, trigger, ...props }: NodeDrawerProps) => {\n  const [open, setOpen] = useControllableValue<boolean>(props, {\n    valuePropName: \"open\",\n    defaultValuePropName: \"defaultOpen\",\n    trigger: \"onOpenChange\",\n  });\n\n  const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) });\n\n  const drawerProps = useMemo(\n    () => ({\n      ...props,\n      node: node!,\n      open: open,\n      onOpenChange: (open: boolean) => {\n        setOpen(open);\n      },\n    }),\n    [props, node, open]\n  );\n\n  return (\n    <>\n      {triggerEl}\n\n      <Show>\n        <Show.Case when={node?.flowNodeType === NodeType.Start}>\n          <StartNodeConfigDrawer {...drawerProps} />\n        </Show.Case>\n        <Show.Case when={node?.flowNodeType === NodeType.Delay}>\n          <DelayNodeConfigDrawer {...drawerProps} />\n        </Show.Case>\n        <Show.Case when={node?.flowNodeType === NodeType.BranchBlock}>\n          <BranchBlockNodeConfigDrawer {...drawerProps} />\n        </Show.Case>\n        <Show.Case when={node?.flowNodeType === NodeType.BizApply}>\n          <BizApplyNodeConfigDrawer {...drawerProps} />\n        </Show.Case>\n        <Show.Case when={node?.flowNodeType === NodeType.BizUpload}>\n          <BizUploadNodeConfigDrawer {...drawerProps} />\n        </Show.Case>\n        <Show.Case when={node?.flowNodeType === NodeType.BizMonitor}>\n          <BizMonitorNodeConfigDrawer {...drawerProps} />\n        </Show.Case>\n        <Show.Case when={node?.flowNodeType === NodeType.BizDeploy}>\n          <BizDeployNodeConfigDrawer {...drawerProps} />\n        </Show.Case>\n        <Show.Case when={node?.flowNodeType === NodeType.BizNotify}>\n          <BizNotifyNodeConfigDrawer {...drawerProps} />\n        </Show.Case>\n        <Show.Default>\n          <></>\n        </Show.Default>\n      </Show>\n    </>\n  );\n};\n\nconst useDrawer = () => {\n  type NodeDataType = NodeDrawerProps[\"node\"];\n  const [node, setNode] = useState<NodeDataType>();\n  const [open, setOpen] = useState(false);\n\n  const onOpenChange = useCallback((open: boolean) => {\n    setOpen(open);\n  }, []);\n\n  return {\n    drawerProps: {\n      afterClose: () => {\n        startTransition(() => {\n          if (!open) {\n            setNode(void 0);\n          }\n        });\n      },\n      node,\n      open,\n      onOpenChange,\n    },\n\n    open: (node: NonNullable<NodeDataType>) => {\n      setNode(node);\n      setOpen(true);\n    },\n    close: () => {\n      setOpen(false);\n    },\n  };\n};\n\nconst _default = Object.assign(NodeDrawer, {\n  useDrawer,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/NodeRender.tsx",
    "content": "import { useEffect } from \"react\";\nimport { type NodeRenderProps, useClientContext, useNodeRender, useRefresh } from \"@flowgram.ai/fixed-layout-editor\";\n\nimport { useDesignerContext } from \"./_context\";\nimport { NodeRenderContextProvider } from \"./NodeRenderContext\";\nimport { type NodeRegistry } from \"./nodes/typings\";\n\nexport interface NodeProps extends NodeRenderProps {}\n\nconst Node = (_: NodeProps) => {\n  const ctx = useClientContext();\n\n  const refresh = useRefresh();\n\n  const nodeRender = useNodeRender();\n\n  const { onDocumentChange: fireOnDocumentChange, onNodeChange: fireOnNodeChange, onNodeClick: fireOnNodeClick } = useDesignerContext();\n\n  useEffect(() => {\n    const d = ctx.document.originTree.onTreeChange(() => refresh());\n\n    return () => d.dispose();\n  }, []);\n\n  useEffect(() => {\n    const d1 = nodeRender.form?.onFormValuesChange?.(() => {\n      refresh();\n\n      fireOnNodeChange(nodeRender.node);\n      fireOnDocumentChange();\n    });\n    const d2 = nodeRender.form?.onValidate?.(() => {\n      refresh();\n    });\n\n    return () => {\n      d1?.dispose();\n      d2?.dispose();\n    };\n  }, [nodeRender.form]);\n\n  const handleNodeClick = () => {\n    const node = nodeRender.node;\n    if (node.getNodeRegistry<NodeRegistry>().meta?.clickable) {\n      fireOnNodeClick(node);\n    }\n  };\n\n  return (\n    <div\n      style={{\n        opacity: nodeRender.dragging ? 0.3 : 1,\n        ...nodeRender.node.getNodeRegistry<NodeRegistry>().meta?.style,\n      }}\n      onMouseEnter={nodeRender.onMouseEnter}\n      onMouseLeave={nodeRender.onMouseLeave}\n      onMouseDown={(e) => {\n        nodeRender.startDrag(e);\n        e.stopPropagation();\n      }}\n      onClick={handleNodeClick}\n    >\n      <NodeRenderContextProvider value={nodeRender}>{nodeRender.form?.render()}</NodeRenderContextProvider>\n    </div>\n  );\n};\n\nexport default Node;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/NodeRenderContext.ts",
    "content": "﻿import { createContext, useContext } from \"react\";\nimport { type NodeRenderReturnType } from \"@flowgram.ai/fixed-layout-editor\";\n\nexport type NodeRenderContextType = NodeRenderReturnType;\n\nexport const NodeRenderContext = createContext<NodeRenderContextType>({} as NodeRenderContextType);\n\nexport const NodeRenderContextProvider = NodeRenderContext.Provider;\n\nexport const useNodeRenderContext = () => {\n  const context = useContext(NodeRenderContext);\n  if (!context) {\n    throw new Error(\"`NodeRenderContext` must be used within a `NodeRenderContextProvider`\");\n  }\n  return context;\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/Toolbar.tsx",
    "content": "import { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { EditorState, FlowLayoutDefault, useClientContext, usePlaygroundTools, useRefresh } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconHandStop, IconLayoutCards, IconMatrix, IconMaximize, IconMinus, IconPlus } from \"@tabler/icons-react\";\nimport { Button, type ButtonProps, Dropdown, Tooltip } from \"antd\";\n\nimport Show from \"@/components/Show\";\nimport { mergeCls } from \"@/utils/css\";\n\nimport Minimap from \"./Minimap\";\n\nexport interface ToolbarProps {\n  className?: string;\n  style?: React.CSSProperties;\n  size?: ButtonProps[\"size\"];\n  showLayout?: boolean;\n  showMinimap?: boolean;\n  showMouseState?: boolean;\n  showZoom?: boolean;\n  showZoomFit?: boolean;\n  showZoomLevel?: boolean;\n}\n\nconst Toolbar = ({\n  className,\n  style,\n  size,\n  showLayout = true,\n  showMinimap = true,\n  showMouseState = true,\n  showZoom = true,\n  showZoomFit = true,\n  showZoomLevel = true,\n}: ToolbarProps) => {\n  const { t } = useTranslation();\n\n  const ctx = useClientContext();\n  const { playground } = ctx;\n\n  const tools = usePlaygroundTools({ minZoom: 0.1, maxZoom: 3 });\n\n  const refresh = useRefresh();\n\n  useEffect(() => {\n    const d = playground.config.onReadonlyOrDisabledChange(() => refresh());\n\n    return () => d.dispose();\n  }, [playground]);\n\n  const buttonIconSize = useMemo(() => {\n    if (size === \"large\") return \"1.5em\";\n    if (size === \"small\") return \"1em\";\n    return \"1.25em\";\n  }, [size]);\n\n  const [isMinimapVisible, setIsMinimapVisible] = useState(() => window.screen.availWidth >= 1024);\n\n  const [isMouseFriendly, setIsMouseFriendly] = useState(() => playground.editorState.is(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id));\n\n  const handleToggleLayout = useCallback(() => {\n    if (tools.isVertical) {\n      tools.changeLayout(FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT);\n    } else {\n      tools.changeLayout(FlowLayoutDefault.VERTICAL_FIXED_LAYOUT);\n    }\n  }, [tools.isVertical]);\n\n  const handleToggleMinimap = useCallback(() => {\n    setIsMinimapVisible((prev) => !prev);\n  }, [isMinimapVisible]);\n\n  const handleToggleMouseFriendly = useCallback(() => {\n    if (isMouseFriendly) {\n      playground.editorState.changeState(EditorState.STATE_SELECT.id);\n      setIsMouseFriendly(false);\n    } else {\n      playground.editorState.changeState(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id);\n      setIsMouseFriendly(true);\n    }\n  }, [isMouseFriendly]);\n\n  return (\n    <div className={className} style={style}>\n      <div className=\"relative flex items-center gap-2\">\n        <Show when={showMouseState}>\n          <Tooltip title={isMouseFriendly ? t(\"workflow.detail.design.toolbar.drag_mode\") : t(\"workflow.detail.design.toolbar.pointer_mode\")}>\n            <Button\n              ghost={isMouseFriendly}\n              icon={<IconHandStop size={buttonIconSize} />}\n              size={size}\n              type={isMouseFriendly ? \"primary\" : \"default\"}\n              onClick={handleToggleMouseFriendly}\n            />\n          </Tooltip>\n        </Show>\n\n        <Show when={showZoom}>\n          <Tooltip title={t(\"workflow.detail.design.toolbar.zoomout\")}>\n            <Button icon={<IconMinus size={buttonIconSize} />} size={size} onClick={() => tools.zoomout()} />\n          </Tooltip>\n          <Show when={showZoomLevel}>\n            <Dropdown\n              menu={{\n                items: [\n                  ...[200, 100, 75, 50, 25].map((zoom) => ({\n                    key: `${zoom}%`,\n                    label: `${zoom}%`,\n                    onClick: () => tools.updateZoom(zoom / 100),\n                  })),\n                  {\n                    type: \"divider\",\n                  },\n                  {\n                    key: \"auto\",\n                    label: t(\"workflow.detail.design.toolbar.auto_fit\"),\n                    onClick: () => tools.fitView(),\n                  },\n                ],\n              }}\n              trigger={[\"click\"]}\n            >\n              <Button className=\"w-16 text-center\" size={size}>\n                {Math.round(tools.zoom * 100)}%\n              </Button>\n            </Dropdown>\n          </Show>\n          <Tooltip title={t(\"workflow.detail.design.toolbar.zoomin\")}>\n            <Button icon={<IconPlus size={buttonIconSize} />} size={size} onClick={() => tools.zoomin()} />\n          </Tooltip>\n          <Show when={showZoomFit}>\n            <Tooltip title={t(\"workflow.detail.design.toolbar.auto_fit\")}>\n              <Button icon={<IconMaximize size={buttonIconSize} />} size={size} onClick={() => tools.fitView()} />\n            </Tooltip>\n          </Show>\n        </Show>\n\n        <Show when={showLayout}>\n          <Tooltip title={tools.isVertical ? t(\"workflow.detail.design.toolbar.vertical_layout\") : t(\"workflow.detail.design.toolbar.horizontal_layout\")}>\n            <Button\n              icon={<IconLayoutCards className={mergeCls({ [\"rotate-90\"]: tools.isVertical })} size={buttonIconSize} />}\n              size={size}\n              onClick={handleToggleLayout}\n            />\n          </Tooltip>\n        </Show>\n\n        <Show when={showMinimap}>\n          <Tooltip title={t(\"workflow.detail.design.toolbar.minimap\")}>\n            <Button\n              icon={<IconMatrix size={buttonIconSize} />}\n              ghost={isMinimapVisible}\n              type={isMinimapVisible ? \"primary\" : \"default\"}\n              size={size}\n              onClick={handleToggleMinimap}\n            />\n          </Tooltip>\n          {isMinimapVisible && <Minimap className=\"absolute right-0 bottom-[42px]\" />}\n        </Show>\n      </div>\n    </div>\n  );\n};\n\nexport default Toolbar;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/_context.ts",
    "content": "﻿import { createContext, useContext } from \"react\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\n\nexport type DesignerContextType = {\n  onDocumentChange: () => void;\n  onNodeChange: (node: FlowNodeEntity) => void;\n  onNodeClick: (node: FlowNodeEntity) => void;\n};\n\nexport const DesignerContext = createContext<DesignerContextType>({\n  onDocumentChange: () => {},\n  onNodeChange: () => {},\n  onNodeClick: () => {},\n});\n\nexport const DegisnerContextProvider = DesignerContext.Provider;\n\nexport const useDesignerContext = () => {\n  const context = useContext(DesignerContext);\n  if (!context) {\n    throw new Error(\"`DesignerContext` must be used within a `DesignerContextProvider`\");\n  }\n  return context;\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/_util.ts",
    "content": "﻿import { FlowNodeBaseType, type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\n\nimport { type WorkflowNode as _WorkflowNode, duplicateNode as _duplicateNode } from \"@/domain/workflow\";\n\nimport { type NodeJSON, NodeType } from \"./nodes/typings\";\n\n/**\n * 克隆节点 JSON 对象。节点及其子节点 ID 均会重新分配。\n * @param {NodeJSON} node\n * @param {Object} options\n * @returns {NodeJSON}\n */\nexport const duplicateNodeJSON = (node: NodeJSON, options?: { withCopySuffix?: boolean }) => {\n  return _duplicateNode(node as _WorkflowNode, options);\n};\n\n/**\n * 获取指定节点到根节点为止的所有前序节点。不包括自身和根节点或开始节点。\n * @param {FlowNodeEntity} node\n * @returns {FlowNodeEntity[]}\n */\nexport const getAllPreviousNodes = (node: FlowNodeEntity): FlowNodeEntity[] => {\n  if (node == null) return [];\n\n  // TODO: 不应该获取到旁路分支\n  // // 先获取单一链路（即不包含分支）的全部节点\n  // const chains: FlowNodeEntity[] = [];\n  // let chain: FlowNodeEntity | undefined = node;\n  // while (chain) {\n  //   if (chain.isStart || chain.flowNodeType === FlowNodeBaseType.ROOT) {\n  //     break;\n  //   }\n\n  //   chains.push(chain);\n  //   chain = chain.pre ?? chain.parent;\n  // }\n\n  // 再获取实际的全部节点\n  const visited = new Set<string>();\n  const result: FlowNodeEntity[] = [];\n  let current: FlowNodeEntity | undefined = node;\n  while (current) {\n    if (current.isStart || current.flowNodeType === FlowNodeBaseType.ROOT) {\n      break;\n    }\n\n    if (current.flowNodeType === NodeType.Condition) {\n      /**\n       * condition\n       *   blockIcon\n       *   inlineBlocks\n       *     branchBlock_1\n       *       blockOrderIcon\n       *       ...\n       *     branchBlock_2\n       *       blockOrderIcon\n       *       ...\n       */\n      current.lastBlock?.blocks?.forEach((block) => {\n        block.allChildren?.forEach((child) => {\n          if (!visited.has(child.id)) {\n            visited.add(child.id);\n            result.push(child);\n          }\n        });\n      });\n    } else if (current.flowNodeType === NodeType.TryCatch) {\n      /**\n       * tryCatch\n       *   blockIcon\n       *   mainInlineBlocks\n       *     tryBlock\n       *       trySlot\n       *       ...\n       *     catchInlineBlocks\n       *       catchBlock_1\n       *         blockOrderIcon\n       *         ...\n       *         end\n       *       catchBlock_2\n       *         blockOrderIcon\n       *         ...\n       *         end\n       */\n      current.lastBlock?.blocks?.forEach((block) => {\n        block.allChildren?.forEach((child) => {\n          if (!visited.has(child.id)) {\n            visited.add(child.id);\n            result.push(child);\n          }\n        });\n      });\n    }\n\n    if (!visited.has(current.id)) {\n      visited.add(current.id);\n      result.push(current);\n    }\n    current = current.pre ?? current.parent;\n  }\n\n  const prevNodes = result.filter((e) => {\n    if (e.id === node.id) return false;\n    if (e.isTypeOrExtendType(FlowNodeBaseType.BLOCK_ICON)) return false;\n    if (e.isTypeOrExtendType(FlowNodeBaseType.BLOCK_ORDER_ICON)) return false;\n    if (e.isTypeOrExtendType(FlowNodeBaseType.INLINE_BLOCKS)) return false;\n    if (e.isTypeOrExtendType(\"trySlot\")) return false;\n\n    return true;\n  });\n  // console.log(node.document.toString());\n  // console.log(node.document.root);\n  // console.log(prevNodes);\n  return prevNodes;\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/elements/Adder.tsx",
    "content": "import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { type AdderProps as FlowgramAdderProps, useClientContext } from \"@flowgram.ai/fixed-layout-editor\";\n\nimport { IconPlus } from \"@tabler/icons-react\";\nimport { Button, Dropdown, type MenuProps } from \"antd\";\n\nimport { getAllNodeRegistries } from \"../nodes\";\n\nexport interface AdderProps extends FlowgramAdderProps {}\n\nconst Adder = ({ from, hoverActivated }: AdderProps) => {\n  const { t } = useTranslation();\n\n  const ctx = useClientContext();\n  const { operation, playground } = ctx;\n\n  const [menuOpen, setMenuOpen] = useState(false); // 使用受控组件，避免下拉菜单展开时鼠标移出而产生的布局抖动\n  const menuItems = getAllNodeRegistries()\n    .filter((registry) => {\n      if (registry.meta?.addDisable != null) {\n        return !registry.meta.addDisable;\n      }\n      return true;\n    })\n    .filter((registry) => {\n      if (registry.canAdd != null) {\n        return registry.canAdd(ctx, from);\n      }\n      return true;\n    })\n    .reduce(\n      (acc, registry) => {\n        let group = acc.find((item) => item!.key === registry.kind);\n        if (!group) {\n          group = {\n            key: registry.kind,\n            type: \"group\",\n            label: registry.kind ? t(`workflow_node.kind.${registry.kind}`) : null,\n            children: [],\n          };\n          acc.push(group);\n        }\n\n        if (group.type === \"group\") {\n          const NodeIcon = registry.meta?.icon;\n          group.children!.push({\n            key: registry.type,\n            label: registry.meta?.labelText ?? registry.type,\n            icon: <span className=\"anticon scale-125\">{NodeIcon && <NodeIcon size=\"1em\" />}</span>,\n            onClick: () => {\n              const block = operation.addFromNode(from, registry.onAdd!(ctx, from));\n\n              setTimeout(() => {\n                playground.scrollToView({\n                  bounds: block.bounds,\n                  scrollToCenter: true,\n                });\n              }, 1);\n            },\n          });\n        }\n\n        return acc;\n      },\n      [] as Required<MenuProps>[\"items\"]\n    );\n\n  return playground.config.readonlyOrDisabled ? null : (\n    <div className=\"relative\">\n      <Dropdown menu={{ items: menuItems }} placement=\"bottomRight\" trigger={[\"click\"]} open={menuOpen} onOpenChange={setMenuOpen}>\n        {hoverActivated || menuOpen ? (\n          <Button icon={<IconPlus size=\"1em\" stroke=\"3\" />} shape=\"circle\" size=\"small\" type=\"primary\" />\n        ) : (\n          <div className=\"size-2 rounded-full bg-primary opacity-75\"></div>\n        )}\n      </Dropdown>\n    </div>\n  );\n};\n\nexport default Adder;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/elements/BranchAdder.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity, type AdderProps as FlowgramAdderProps, useClientContext } from \"@flowgram.ai/fixed-layout-editor\";\nimport { Button } from \"antd\";\n\nimport { BranchBlockNodeRegistry } from \"../nodes/ConditionNode\";\nimport { CatchBlockNodeRegistry } from \"../nodes/TryCatchNode\";\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface BranchAdderProps extends FlowgramAdderProps {}\n\nconst BranchAdder = ({ node }: BranchAdderProps) => {\n  const { t } = useTranslation();\n\n  const ctx = useClientContext();\n  const { operation, playground } = ctx;\n\n  const handleAddBranch = () => {\n    let block: FlowNodeEntity;\n    switch (node.flowNodeType) {\n      case NodeType.Condition:\n        {\n          block = operation.addBlock(node, BranchBlockNodeRegistry.onAdd!(ctx, node));\n        }\n        break;\n\n      case NodeType.TryCatch:\n        {\n          block = operation.addBlock(node, CatchBlockNodeRegistry.onAdd!(ctx, node));\n        }\n        break;\n\n      default:\n        console.warn(`[certimate] unsupported node type for adding branch: '${node.flowNodeType}'`);\n        break;\n    }\n\n    setTimeout(() => {\n      playground.scrollToView({\n        bounds: block.bounds,\n        scrollToCenter: true,\n      });\n    }, 1);\n  };\n\n  // TryCatch 暂不支持添加分支\n  return playground.config.readonlyOrDisabled || node.flowNodeType === NodeType.TryCatch ? null : (\n    <div\n      className=\"relative\"\n      onMouseEnter={() => node.firstChild?.renderData?.toggleMouseEnter()}\n      onMouseLeave={() => node.firstChild?.renderData?.toggleMouseLeave()}\n    >\n      <Button shape=\"round\" size=\"small\" onClick={handleAddBranch}>\n        <span className=\"text-xs\">{t(\"workflow.detail.design.editor.add_branch\")}</span>\n      </Button>\n    </div>\n  );\n};\n\nexport default BranchAdder;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/elements/Collapse.tsx",
    "content": "import { type CollapseProps as FlowgramCollapseProps } from \"@flowgram.ai/fixed-layout-editor\";\n\nexport interface CollapseProps extends FlowgramCollapseProps {}\n\nconst Collapse = (_: CollapseProps) => {\n  return null;\n};\n\nexport default Collapse;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/elements/DragHighlightAdder.tsx",
    "content": "import { type DragNodeProps as FlowgramDragNodeProps } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconGradienter } from \"@tabler/icons-react\";\n\nconst DragHighlightAdder = (_: FlowgramDragNodeProps) => {\n  return (\n    <div className=\"size-4 animate-ping rounded-full bg-primary text-white shadow-sm\">\n      <div className=\"flex size-full items-center justify-center\">\n        <IconGradienter size=\"1em\" />\n      </div>\n    </div>\n  );\n};\n\nexport default DragHighlightAdder;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/elements/DragNode.tsx",
    "content": "import { type FlowNodeEntity, type DragNodeProps as FlowgramDragNodeProps } from \"@flowgram.ai/fixed-layout-editor\";\nimport { Badge, Card } from \"antd\";\n\nexport interface DragNodeProps extends FlowgramDragNodeProps {\n  dragStart: FlowNodeEntity;\n  dragNodes: FlowNodeEntity[];\n}\n\nconst DragNode = ({ dragStart, dragNodes }: DragNodeProps) => {\n  const count = (dragNodes || [])\n    .map((n) => (n.allCollapsedChildren.length ? n.allCollapsedChildren.filter((_n) => !_n.hidden).length : 1))\n    .reduce((acc, cur) => acc + cur, 0);\n  return (\n    <Badge count={count > 1 ? count : 0} size=\"small\">\n      <div className=\"relative w-[160px]\">\n        <Card className=\"bg-transparent shadow-sm\" styles={{ body: { padding: 0 } }}>\n          <div className=\"overflow-hidden px-4 py-2 text-primary\">\n            <div className=\"truncate\">{dragStart ? dragStart.form?.getValueIn(\"name\") || `#${dragStart?.id}` : \"\\u00A0\"}</div>\n          </div>\n        </Card>\n      </div>\n    </Badge>\n  );\n};\n\nexport default DragNode;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/elements/DraggingAdder.tsx",
    "content": "import { FlowDragLayer, type AdderProps as FlowgramAdderProps, usePlayground } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconChevronsDown } from \"@tabler/icons-react\";\n\nexport interface DraggingAdderProps extends FlowgramAdderProps {}\n\nconst DraggingAdder = ({ from }: DraggingAdderProps) => {\n  const playground = usePlayground();\n\n  const layer = playground.getLayer(FlowDragLayer);\n  if (!layer) return <></>;\n  if (\n    layer.options.canDrop &&\n    !layer.options.canDrop({\n      dragNodes: layer.dragEntities ?? [],\n      dropNode: from,\n      isBranch: false,\n    })\n  ) {\n    return <></>;\n  }\n\n  return (\n    <div className=\"size-4 animate-bounce rounded-full bg-primary text-white shadow-sm\">\n      <div className=\"flex size-full items-center justify-center\">\n        <IconChevronsDown size=\"1em\" />\n      </div>\n    </div>\n  );\n};\n\nexport default DraggingAdder;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/elements/Null.tsx",
    "content": "import { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\n\nexport interface NullProps {\n  node: FlowNodeEntity;\n}\n\nconst Null = (_: NullProps) => {\n  return null;\n};\n\nexport default Null;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/elements/TryCatchCollapse.tsx",
    "content": "import {\n  type CustomLabelProps,\n  FlowNodeRenderData,\n  FlowNodeTransformData,\n  FlowRendererRegistry,\n  FlowTextKey,\n  useBaseColor,\n} from \"@flowgram.ai/fixed-layout-editor\";\n\nexport interface TryCatchCollapseProps extends CustomLabelProps {}\n\nconst TryCatchCollapse = ({ node, ...props }: TryCatchCollapseProps) => {\n  const { baseColor, baseActivatedColor } = useBaseColor();\n\n  const nodeRenderData = node.getData(FlowNodeRenderData)!;\n  const nodeTransformData = node.getData(FlowNodeTransformData)!;\n\n  const handleMouseEnter = () => {\n    nodeRenderData.activated = true;\n  };\n\n  const handleMouseLeave = () => {\n    nodeRenderData.activated = false;\n  };\n\n  if (!nodeTransformData || !nodeTransformData.parent) {\n    return <></>;\n  }\n\n  const width = nodeTransformData.inputPoint.x - nodeTransformData.parent.inputPoint.x;\n  const height = 40;\n  return (\n    <div\n      onMouseEnter={handleMouseEnter}\n      onMouseLeave={handleMouseLeave}\n      style={{\n        width,\n        height,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        gap: 6,\n      }}\n    >\n      <div\n        data-label-id={props.labelId}\n        style={{\n          fontSize: 12,\n          color: nodeRenderData.activated || nodeRenderData.lineActivated ? baseActivatedColor : baseColor,\n          textAlign: \"center\",\n          lineHeight: \"20px\",\n          whiteSpace: \"nowrap\",\n          backgroundColor: \"var(--g-editor-background)\",\n        }}\n      >\n        {node.getService(FlowRendererRegistry).getText(FlowTextKey.CATCH_TEXT)}\n      </div>\n    </div>\n  );\n};\n\nexport default TryCatchCollapse;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/elements/index.ts",
    "content": "﻿import { FlowRendererKey } from \"@flowgram.ai/fixed-layout-editor\";\n\nimport Adder from \"./Adder\";\nimport BranchAdder from \"./BranchAdder\";\nimport Collapse from \"./Collapse\";\nimport DraggingAdder from \"./DraggingAdder\";\nimport DragHighlightAdder from \"./DragHighlightAdder\";\nimport DragNode from \"./DragNode\";\nimport Null from \"./Null\";\nimport TryCatchCollapse from \"./TryCatchCollapse\";\n\nexport const getAllElements = () => {\n  return {\n    [FlowRendererKey.ADDER]: Adder,\n    [FlowRendererKey.BRANCH_ADDER]: BranchAdder,\n    [FlowRendererKey.SLOT_ADDER]: Null,\n\n    [FlowRendererKey.COLLAPSE]: Collapse,\n    [FlowRendererKey.TRY_CATCH_COLLAPSE]: TryCatchCollapse,\n    [FlowRendererKey.SLOT_COLLAPSE]: Null,\n\n    [FlowRendererKey.DRAG_NODE]: DragNode,\n    [FlowRendererKey.DRAG_HIGHLIGHT_ADDER]: DragHighlightAdder,\n    [FlowRendererKey.DRAG_BRANCH_HIGHLIGHT_ADDER]: DragHighlightAdder,\n    [FlowRendererKey.DRAGGABLE_ADDER]: DraggingAdder,\n\n    [FlowRendererKey.SELECTOR_BOX_POPOVER]: Null,\n  };\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/flowgram.css",
    "content": "﻿/* 隐藏 flowgram 的标签背景 */\n/* flowgram 暂未提供相关配置项，这里需与 antd.colorBgContainer 保持一致 */\n:root {\n  --g-editor-background: #fff;\n}\n.dark {\n  --g-editor-background: #262c2d;\n}\n\n/* 隐藏 flowgram 的折叠按钮 */\n.flow-canvas-collapse-adder > .flow-canvas-collapse {\n  display: hidden !important;\n  position: absolute !important;\n  top: -99999px !important;\n  left: -99999px !important;\n  width: 0 !important;\n  height: 0 !important;\n  visibility: hidden !important;\n  transform: scale(0) !important;\n}\n\n/* 控制 flowgram 线条样式 */\n/* flowgram 暂未提供相关配置项 */\n.flow-lines-container {\n  stroke-width: 1.5px;\n}\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizApplyNodeConfigDrawer.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { Form } from \"antd\";\n\nimport { type WorkflowNodeConfigForBizApply } from \"@/domain/workflow\";\n\nimport { NodeConfigDrawer } from \"./_shared\";\nimport BizApplyNodeConfigForm from \"./BizApplyNodeConfigForm\";\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface BizApplyNodeConfigDrawerProps {\n  afterClose?: () => void;\n  loading?: boolean;\n  node: FlowNodeEntity;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst BizApplyNodeConfigDrawer = ({ node, ...props }: BizApplyNodeConfigDrawerProps) => {\n  if (node.flowNodeType !== NodeType.BizApply) {\n    console.warn(`[certimate] current workflow node type is not: ${NodeType.BizApply}`);\n  }\n\n  const { i18n } = useTranslation();\n\n  const [formInst] = Form.useForm();\n\n  const fieldIdentifier = Form.useWatch<WorkflowNodeConfigForBizApply[\"identifier\"]>(\"identifier\", { form: formInst, preserve: true });\n\n  return (\n    <NodeConfigDrawer\n      anchor={fieldIdentifier ? { items: BizApplyNodeConfigForm.getAnchorItems({ i18n }) } : false}\n      footer={fieldIdentifier ? void 0 : false}\n      form={formInst}\n      node={node}\n      {...props}\n    >\n      <BizApplyNodeConfigForm form={formInst} node={node} />\n    </NodeConfigDrawer>\n  );\n};\n\nexport default BizApplyNodeConfigDrawer;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProvider.tsx",
    "content": "import { useEffect, useState } from \"react\";\n\nimport { type ACMEDns01ProviderType, type ACMEHttp01ProviderType, ACME_DNS01_PROVIDERS, ACME_HTTP01_PROVIDERS } from \"@/domain/provider\";\n\nimport BizApplyNodeConfigFieldsProviderAliyunESA from \"./BizApplyNodeConfigFieldsProviderAliyunESA\";\nimport BizApplyNodeConfigFieldsProviderAWSRoute53 from \"./BizApplyNodeConfigFieldsProviderAWSRoute53\";\nimport BizApplyNodeConfigFieldsProviderHuaweiCloudDNS from \"./BizApplyNodeConfigFieldsProviderHuaweiCloudDNS\";\nimport BizApplyNodeConfigFieldsProviderJDCloudDNS from \"./BizApplyNodeConfigFieldsProviderJDCloudDNS\";\nimport BizApplyNodeConfigFieldsProviderLocal from \"./BizApplyNodeConfigFieldsProviderLocal\";\nimport BizApplyNodeConfigFieldsProviderS3 from \"./BizApplyNodeConfigFieldsProviderS3\";\nimport BizApplyNodeConfigFieldsProviderSSH from \"./BizApplyNodeConfigFieldsProviderSSH\";\n\nconst acmeDns01ProviderComponentMap: Partial<Record<ACMEDns01ProviderType, React.ComponentType<any>>> = {\n  /*\n    注意：如果追加新的子组件，请保持以 ASCII 排序。\n    NOTICE: If you add new child component, please keep ASCII order.\n    */\n  [ACME_DNS01_PROVIDERS.ALIYUN_ESA]: BizApplyNodeConfigFieldsProviderAliyunESA,\n  [ACME_DNS01_PROVIDERS.AWS]: BizApplyNodeConfigFieldsProviderAWSRoute53,\n  [ACME_DNS01_PROVIDERS.AWS_ROUTE53]: BizApplyNodeConfigFieldsProviderAWSRoute53,\n  [ACME_DNS01_PROVIDERS.HUAWEICLOUD]: BizApplyNodeConfigFieldsProviderHuaweiCloudDNS,\n  [ACME_DNS01_PROVIDERS.HUAWEICLOUD_DNS]: BizApplyNodeConfigFieldsProviderHuaweiCloudDNS,\n  [ACME_DNS01_PROVIDERS.JDCLOUD]: BizApplyNodeConfigFieldsProviderJDCloudDNS,\n  [ACME_DNS01_PROVIDERS.JDCLOUD_DNS]: BizApplyNodeConfigFieldsProviderJDCloudDNS,\n};\n\nconst acmeHttp01ProviderComponentMap: Partial<Record<ACMEHttp01ProviderType, React.ComponentType<any>>> = {\n  /*\n    注意：如果追加新的子组件，请保持以 ASCII 排序。\n    NOTICE: If you add new child component, please keep ASCII order.\n    */\n  [ACME_HTTP01_PROVIDERS.LOCAL]: BizApplyNodeConfigFieldsProviderLocal,\n  [ACME_HTTP01_PROVIDERS.S3]: BizApplyNodeConfigFieldsProviderS3,\n  [ACME_HTTP01_PROVIDERS.SSH]: BizApplyNodeConfigFieldsProviderSSH,\n};\n\nconst useComponent = (\n  challenge: \"dns-01\" | \"http-01\",\n  provider: string,\n  { initProps, deps = [] }: { initProps?: (provider: string) => any; deps?: unknown[] }\n) => {\n  const initComponent = () => {\n    const Component =\n      challenge === \"dns-01\"\n        ? acmeDns01ProviderComponentMap[provider as ACMEDns01ProviderType]\n        : challenge === \"http-01\"\n          ? acmeHttp01ProviderComponentMap[provider as ACMEHttp01ProviderType]\n          : void 0;\n    if (!Component) return null;\n\n    const props = initProps?.(provider);\n    if (props) {\n      return <Component {...props} />;\n    }\n\n    return <Component />;\n  };\n\n  const [component, setComponent] = useState(() => initComponent());\n\n  useEffect(() => setComponent(initComponent()), [challenge, provider]);\n  useEffect(() => setComponent(initComponent()), deps);\n\n  return component;\n};\n\nconst _default = {\n  useComponent,\n};\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderAWSRoute53.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizApplyNodeConfigFieldsProviderAWSRoute53 = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.apply.form.aws_route53_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.aws_route53_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.apply.form.aws_route53_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"hostedZoneId\"]}\n        initialValue={initialValues.hostedZoneId}\n        label={t(\"workflow_node.apply.form.aws_route53_hosted_zone_id.label\")}\n        extra={t(\"workflow_node.apply.form.aws_route53_hosted_zone_id.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.aws_route53_hosted_zone_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.apply.form.aws_route53_hosted_zone_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"us-east-1\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.apply.form.aws_route53_region.placeholder\")),\n    hostedZoneId: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(BizApplyNodeConfigFieldsProviderAWSRoute53, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderAliyunESA.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizApplyNodeConfigFieldsProviderAliyunESA = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.apply.form.aliyun_esa_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.aliyun_esa_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.apply.form.aliyun_esa_region.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"cn-hangzhou\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.apply.form.aliyun_esa_region.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(BizApplyNodeConfigFieldsProviderAliyunESA, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderHuaweiCloudDNS.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizApplyNodeConfigFieldsProviderHuaweiCloudDNS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.apply.form.huaweicloud_dns_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.huaweicloud_dns_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.apply.form.huaweicloud_dns_region.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"cn-north-1\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.apply.form.huaweicloud_dns_region.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(BizApplyNodeConfigFieldsProviderHuaweiCloudDNS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderJDCloudDNS.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizApplyNodeConfigFieldsProviderJDCloudDNS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"regionId\"]}\n        initialValue={initialValues.regionId}\n        label={t(\"workflow_node.apply.form.jdcloud_dns_region_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.jdcloud_dns_region_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.apply.form.jdcloud_dns_region_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    regionId: \"cn-north-1\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    regionId: z.string().nonempty(t(\"workflow_node.apply.form.jdcloud_dns_region_id.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(BizApplyNodeConfigFieldsProviderJDCloudDNS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderLocal.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizApplyNodeConfigFieldsProviderLocal = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"webRootPath\"]}\n        initialValue={initialValues.webRootPath}\n        label={t(\"workflow_node.apply.form.local_webroot_path.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.local_webroot_path.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.apply.form.local_webroot_path.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    webRootPath: \"/var/www/html/\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    webRootPath: z\n      .string()\n      .nonempty(t(\"workflow_node.apply.form.local_webroot_path.placeholder\"))\n      .refine((v) => !!v && (v.endsWith(\"/\") || v.endsWith(\"\\\\\")), {\n        error: t(\"workflow_node.apply.form.local_webroot_path.placeholder\"),\n      }),\n  });\n};\n\nconst _default = Object.assign(BizApplyNodeConfigFieldsProviderLocal, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderS3.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizApplyNodeConfigFieldsProviderS3 = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item name={[parentNamePath, \"region\"]} initialValue={initialValues.region} label={t(\"workflow_node.apply.form.s3_region.label\")} rules={[formRule]}>\n        <Input placeholder={t(\"workflow_node.apply.form.s3_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"bucket\"]} initialValue={initialValues.bucket} label={t(\"workflow_node.apply.form.s3_bucket.label\")} rules={[formRule]}>\n        <Input placeholder={t(\"workflow_node.apply.form.s3_bucket.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    bucket: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.apply.form.s3_region.placeholder\")),\n    bucket: z.string().nonempty(t(\"workflow_node.apply.form.s3_bucket.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(BizApplyNodeConfigFieldsProviderS3, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderSSH.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizApplyNodeConfigFieldsProviderSSH = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"webRootPath\"]}\n        initialValue={initialValues.webRootPath}\n        label={t(\"workflow_node.apply.form.ssh_webroot_path.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.ssh_webroot_path.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.apply.form.ssh_webroot_path.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"useSCP\"]}\n        initialValue={initialValues.useSCP}\n        label={t(\"workflow_node.apply.form.ssh_use_scp.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.ssh_use_scp.tooltip\") }}></span>}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    webRootPath: \"/var/www/html/\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    webRootPath: z\n      .string()\n      .nonempty(t(\"workflow_node.apply.form.ssh_webroot_path.placeholder\"))\n      .refine((v) => !!v && (v.endsWith(\"/\") || v.endsWith(\"\\\\\")), {\n        error: t(\"workflow_node.apply.form.ssh_webroot_path.placeholder\"),\n      }),\n    useSCP: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(BizApplyNodeConfigFieldsProviderSSH, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx",
    "content": "import { memo, useEffect, useMemo, useState } from \"react\";\nimport { getI18n, useTranslation } from \"react-i18next\";\nimport { Link } from \"react-router\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconArrowRight, IconChevronRight, IconCircleMinus, IconMapPin, IconPlus, IconWorldWww } from \"@tabler/icons-react\";\nimport { useControllableValue, useMount } from \"ahooks\";\nimport {\n  type AnchorProps,\n  AutoComplete,\n  Avatar,\n  Button,\n  Card,\n  Divider,\n  Form,\n  type FormInstance,\n  Input,\n  InputNumber,\n  Radio,\n  Select,\n  Space,\n  Switch,\n  Typography,\n} from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport AccessEditDrawer from \"@/components/access/AccessEditDrawer\";\nimport AccessSelect from \"@/components/access/AccessSelect\";\nimport FileTextInput from \"@/components/FileTextInput\";\nimport MultipleSplitValueInput from \"@/components/MultipleSplitValueInput\";\nimport ACMEDns01ProviderSelect from \"@/components/provider/ACMEDns01ProviderSelect\";\nimport ACMEHttp01ProviderSelect from \"@/components/provider/ACMEHttp01ProviderSelect\";\nimport CAProviderSelect from \"@/components/provider/CAProviderSelect\";\nimport Show from \"@/components/Show\";\nimport { type AccessModel } from \"@/domain/access\";\nimport { CA_PROVIDERS, acmeDns01ProvidersMap, acmeHttp01ProvidersMap, caProvidersMap } from \"@/domain/provider\";\nimport { type WorkflowNodeConfigForBizApply, defaultNodeConfigForBizApply } from \"@/domain/workflow\";\nimport { useAntdForm, useZustandShallowSelector } from \"@/hooks\";\nimport { useAccessesStore } from \"@/stores/access\";\nimport { useContactEmailsStore } from \"@/stores/settings\";\nimport { mergeCls } from \"@/utils/css\";\nimport { matchSearchOption } from \"@/utils/search\";\nimport { isDomain, isHostname, isIPv4, isIPv6 } from \"@/utils/validator\";\nimport { getPrivateKeyAlgorithm as getPKIXPrivateKeyAlgorithm, validatePEMPrivateKey } from \"@/utils/x509\";\n\nimport { FormNestedFieldsContextProvider, NodeFormContextProvider } from \"./_context\";\nimport BizApplyNodeConfigFieldsProvider from \"./BizApplyNodeConfigFieldsProvider\";\nimport { NodeType } from \"../nodes/typings\";\n\nconst MULTIPLE_INPUT_SEPARATOR = \";\";\n\nconst IDENTIFIER_DOMAIN = \"domain\" as const;\nconst IDENTIFIER_IP = \"ip\" as const;\n\nconst CHALLENGE_TYPE_DNS01 = \"dns-01\" as const;\nconst CHALLENGE_TYPE_HTTP01 = \"http-01\" as const;\n\nconst KEY_SOURCE_AUTO = \"auto\" as const;\nconst KEY_SOURCE_REUSE = \"reuse\" as const;\nconst KEY_SOURCE_CUSTOM = \"custom\" as const;\n\nexport interface BizApplyNodeConfigFormProps {\n  form: FormInstance;\n  node: FlowNodeEntity;\n}\n\nconst BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps) => {\n  if (node.flowNodeType !== NodeType.BizApply) {\n    console.warn(`[certimate] current workflow node type is not: ${NodeType.BizApply}`);\n  }\n\n  const { i18n, t } = useTranslation();\n\n  const { accesses } = useAccessesStore(useZustandShallowSelector(\"accesses\"));\n  const accessOptionFilter = (_: string, option: AccessModel) => {\n    if (option.reserve) return false;\n    if (fieldChallengeType === CHALLENGE_TYPE_DNS01) return acmeDns01ProvidersMap.get(fieldProvider)?.provider === option.provider;\n    if (fieldChallengeType === CHALLENGE_TYPE_HTTP01) return acmeHttp01ProvidersMap.get(fieldProvider)?.provider === option.provider;\n    return false;\n  };\n  const accessOptionFilterForCA = (_: string, option: AccessModel) => {\n    if (option.reserve !== \"ca\") return false;\n    return caProvidersMap.get(fieldCAProvider!)?.provider === option.provider;\n  };\n\n  const initialValues = useMemo(() => {\n    return node.form?.getValueIn(\"config\") as WorkflowNodeConfigForBizApply | undefined;\n  }, [node]);\n\n  const formSchema = getSchema({ i18n });\n  type FormSchema = z.infer<typeof formSchema>;\n  const formRule = createSchemaFieldRule(formSchema);\n  const { form: formInst, formProps } = useAntdForm<FormSchema>({\n    form: props.form,\n    name: \"workflowNodeBizApplyConfigForm\",\n    initialValues: initialValues ?? getInitialValues(),\n  });\n\n  const fieldIdentifier = Form.useWatch(\"identifier\", { form: formInst, preserve: true });\n  const fieldChallengeType = Form.useWatch(\"challengeType\", { form: formInst, preserve: true });\n  const fieldProvider = Form.useWatch(\"provider\", { form: formInst, preserve: true });\n  const fieldProviderAccessId = Form.useWatch(\"providerAccessId\", { form: formInst, preserve: true });\n  const fieldKeySource = Form.useWatch(\"keySource\", { form: formInst, preserve: true });\n  const fieldCAProvider = Form.useWatch(\"caProvider\", { form: formInst, preserve: true });\n  const fieldCAProviderAccessId = Form.useWatch(\"caProviderAccessId\", { form: formInst, preserve: true });\n\n  const renderNestedFieldProviderComponent = BizApplyNodeConfigFieldsProvider.useComponent(fieldChallengeType, fieldProvider, {});\n\n  const resetFieldIfInvalid = (field: keyof FormSchema) => {\n    const fieldSchema = formSchema.pick({ [field]: true } as Record<keyof FormSchema, true>);\n    const fieldValue = formInst.getFieldValue(field);\n    if (!fieldSchema.safeParse({ [field]: fieldValue }).success) {\n      formInst.setFieldValue(field, void 0);\n    }\n  };\n\n  const showProviderAccess = useMemo(() => {\n    // 内置的质询提供商（如本地主机）无需显示授权信息字段\n    switch (fieldChallengeType) {\n      case CHALLENGE_TYPE_DNS01:\n        {\n          if (fieldProvider) {\n            const provider = acmeDns01ProvidersMap.get(fieldProvider);\n            return !provider?.builtin;\n          }\n        }\n        break;\n\n      case CHALLENGE_TYPE_HTTP01:\n        {\n          if (fieldProvider) {\n            const provider = acmeHttp01ProvidersMap.get(fieldProvider);\n            return !provider?.builtin;\n          }\n        }\n        break;\n    }\n\n    return false;\n  }, [fieldChallengeType, fieldProvider]);\n\n  const showCAProviderAccess = useMemo(() => {\n    // 内置的 CA 提供商（如 Let's Encrypt）无需显示授权信息字段\n    if (fieldCAProvider) {\n      const provider = caProvidersMap.get(fieldCAProvider);\n      return !provider?.builtin;\n    }\n\n    return false;\n  }, [fieldCAProvider]);\n\n  useEffect(() => {\n    // 如果未选择质询提供商，则清空授权信息\n    if (!fieldProvider && fieldProviderAccessId) {\n      formInst.setFieldValue(\"providerAccessId\", void 0);\n      return;\n    }\n\n    // 如果已选择质询提供商只有一个授权信息，则自动选择该授权信息\n    if (fieldProvider && !fieldProviderAccessId) {\n      const availableAccesses = accesses\n        .filter((access) => accessOptionFilter(access.provider, access))\n        .filter((access) => {\n          if (fieldChallengeType === CHALLENGE_TYPE_DNS01) return acmeDns01ProvidersMap.get(fieldProvider)?.provider === access.provider;\n          if (fieldChallengeType === CHALLENGE_TYPE_HTTP01) return acmeHttp01ProvidersMap.get(fieldProvider)?.provider === access.provider;\n          return false;\n        });\n      if (availableAccesses.length === 1) {\n        formInst.setFieldValue(\"providerAccessId\", availableAccesses[0].id);\n      }\n    }\n  }, [fieldChallengeType, fieldProvider, fieldProviderAccessId]);\n\n  useEffect(() => {\n    // 如果未选择 CA 提供商，则清空授权信息\n    if (!fieldCAProvider && fieldCAProviderAccessId) {\n      formInst.setFieldValue(\"caProviderAccessId\", void 0);\n      return;\n    }\n\n    // 如果已选择 CA 提供商只有一个授权信息，则自动选择该授权信息\n    if (fieldCAProvider && !fieldCAProviderAccessId) {\n      const availableAccesses = accesses\n        .filter((access) => accessOptionFilterForCA(access.provider, access))\n        .filter((access) => caProvidersMap.get(fieldCAProvider)?.provider === access.provider);\n      if (availableAccesses.length === 1) {\n        formInst.setFieldValue(\"caProviderAccessId\", availableAccesses[0].id);\n      }\n    }\n  }, [fieldCAProvider, fieldCAProviderAccessId]);\n\n  const handleIdentifierPick = (value: string) => {\n    switch (value) {\n      case IDENTIFIER_DOMAIN:\n        {\n          formInst.setFieldValue(\"identifier\", IDENTIFIER_DOMAIN);\n          formInst.setFieldValue(\"domains\", formInst.getFieldValue(\"domains\") || \"\");\n          formInst.setFieldValue(\"challengeType\", CHALLENGE_TYPE_DNS01);\n        }\n        break;\n\n      case IDENTIFIER_IP:\n        {\n          formInst.setFieldValue(\"identifier\", IDENTIFIER_IP);\n          formInst.setFieldValue(\"ipaddrs\", formInst.getFieldValue(\"ipaddrs\") || \"\");\n          formInst.setFieldValue(\"challengeType\", CHALLENGE_TYPE_HTTP01);\n          formInst.setFieldValue(\"caProvider\", CA_PROVIDERS.LETSENCRYPT);\n          formInst.setFieldValue(\"caProviderAccessId\", void 0);\n          formInst.setFieldValue(\"caProviderConfig\", void 0);\n          formInst.setFieldValue(\"acmeProfile\", \"shortlived\");\n          formInst.setFieldValue(\"disableCommonName\", true);\n          formInst.setFieldValue(\"skipBeforeExpiryDays\", 3);\n        }\n        break;\n    }\n\n    setTimeout(() => handleIdentifierChange(value), 0);\n  };\n\n  const handleIdentifierChange = (value: string) => {\n    switch (value) {\n      case IDENTIFIER_DOMAIN:\n        {\n          formInst.setFieldValue(\"ipaddrs\", void 0);\n        }\n        break;\n\n      case IDENTIFIER_IP:\n        {\n          formInst.setFieldValue(\"domains\", void 0);\n\n          resetFieldIfInvalid(\"nameservers\");\n        }\n        break;\n    }\n  };\n\n  const handleChallengeTypeChange = (value: string) => {\n    switch (value) {\n      case CHALLENGE_TYPE_DNS01:\n        {\n          formInst.setFieldValue(\"provider\", void 0);\n          formInst.setFieldValue(\"providerAccessId\", void 0);\n          formInst.setFieldValue(\"providerConfig\", void 0);\n\n          resetFieldIfInvalid(\"httpDelayWait\");\n        }\n        break;\n\n      case CHALLENGE_TYPE_HTTP01:\n        {\n          formInst.setFieldValue(\"provider\", void 0);\n          formInst.setFieldValue(\"providerAccessId\", void 0);\n          formInst.setFieldValue(\"providerConfig\", void 0);\n\n          resetFieldIfInvalid(\"dnsPropagationWait\");\n          resetFieldIfInvalid(\"dnsPropagationTimeout\");\n          resetFieldIfInvalid(\"dnsTTL\");\n        }\n        break;\n    }\n  };\n\n  const handleProviderSelect = (value?: string | undefined) => {\n    // 切换质询提供商时重置表单，避免其他提供商的配置字段影响当前提供商\n    if (initialValues?.provider === value) {\n      formInst.setFieldValue(\"providerAccessId\", void 0);\n      formInst.resetFields([\"providerConfig\"]);\n    } else {\n      formInst.setFieldValue(\"providerAccessId\", void 0);\n      formInst.setFieldValue(\"providerConfig\", void 0);\n    }\n  };\n\n  const handleKeySourceChange = (value: string) => {\n    if (value === initialValues?.keySource) {\n      formInst.resetFields([\"keyContent\"]);\n    } else {\n      setTimeout(() => {\n        formInst.setFieldValue(\"keyContent\", \"\");\n      }, 0);\n    }\n  };\n\n  const handleCAProviderSelect = (value?: string | undefined) => {\n    // 切换 CA 提供商时联动授权信息\n    if (value == null || value === \"\") {\n      formInst.setFieldValue(\"caProvider\", void 0);\n      formInst.setFieldValue(\"caProviderAccessId\", void 0);\n    } else if (value === initialValues?.caProvider) {\n      formInst.setFieldValue(\"caProviderAccessId\", initialValues?.caProviderAccessId);\n    } else {\n      if (caProvidersMap.get(fieldCAProvider!)?.provider !== caProvidersMap.get(value!)?.provider) {\n        formInst.setFieldValue(\"caProviderAccessId\", void 0);\n      }\n    }\n  };\n\n  return (\n    <NodeFormContextProvider value={{ node }}>\n      <Form {...formProps} clearOnDestroy={true} form={formInst} layout=\"vertical\" preserve={false} scrollToFirstError>\n        <Show when={!fieldIdentifier}>\n          <InternalIdentifierPicker onSelect={handleIdentifierPick} />\n        </Show>\n\n        <div style={{ display: fieldIdentifier ? \"block\" : \"none\" }}>\n          <div id=\"parameters\" data-anchor=\"parameters\">\n            <Form.Item name=\"identifier\" hidden label={t(\"workflow_node.apply.form.identifier.label\")} rules={[formRule]}>\n              <Radio.Group block onChange={(e) => handleIdentifierChange(e.target.value)}>\n                <Radio.Button value={IDENTIFIER_DOMAIN}>{t(\"workflow_node.apply.form.identifier.option.domain.label\")}</Radio.Button>\n                <Radio.Button value={IDENTIFIER_IP}>{t(\"workflow_node.apply.form.identifier.option.ip.label\")}</Radio.Button>\n              </Radio.Group>\n            </Form.Item>\n\n            <Show>\n              <Show.Case when={fieldIdentifier === IDENTIFIER_DOMAIN}>\n                <Form.Item\n                  name=\"domains\"\n                  dependencies={[\"identifier\", \"challengeType\"]}\n                  label={t(\"workflow_node.apply.form.domains.label\")}\n                  extra={\n                    <span\n                      dangerouslySetInnerHTML={{\n                        __html:\n                          fieldChallengeType === CHALLENGE_TYPE_HTTP01\n                            ? t(\"workflow_node.apply.form.domains.help_no_wildcard\")\n                            : t(\"workflow_node.apply.form.domains.help\"),\n                      }}\n                    ></span>\n                  }\n                  rules={[formRule]}\n                >\n                  <MultipleSplitValueInput\n                    modalTitle={t(\"workflow_node.apply.form.domains.multiple_input_modal.title\")}\n                    placeholder={t(\"workflow_node.apply.form.domains.placeholder\")}\n                    placeholderInModal={t(\"workflow_node.apply.form.domains.multiple_input_modal.placeholder\")}\n                    separator={MULTIPLE_INPUT_SEPARATOR}\n                    splitOptions={{ removeEmpty: true, trimSpace: true }}\n                  />\n                </Form.Item>\n              </Show.Case>\n              <Show.Case when={fieldIdentifier === IDENTIFIER_IP}>\n                <Form.Item\n                  name=\"ipaddrs\"\n                  dependencies={[\"identifier\", \"challengeType\"]}\n                  label={t(\"workflow_node.apply.form.ipaddrs.label\")}\n                  extra={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.ipaddrs.help\") }}></span>}\n                  rules={[formRule]}\n                >\n                  <MultipleSplitValueInput\n                    modalTitle={t(\"workflow_node.apply.form.ipaddrs.multiple_input_modal.title\")}\n                    placeholder={t(\"workflow_node.apply.form.ipaddrs.placeholder\")}\n                    placeholderInModal={t(\"workflow_node.apply.form.ipaddrs.multiple_input_modal.placeholder\")}\n                    separator={MULTIPLE_INPUT_SEPARATOR}\n                    splitOptions={{ removeEmpty: true, trimSpace: true }}\n                  />\n                </Form.Item>\n              </Show.Case>\n            </Show>\n\n            <Form.Item\n              name=\"contactEmail\"\n              label={t(\"workflow_node.apply.form.contact_email.label\")}\n              rules={[formRule]}\n              tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.contact_email.tooltip\") }}></span>}\n            >\n              <InternalEmailInput />\n            </Form.Item>\n          </div>\n\n          <div id=\"challenge\" data-anchor=\"challenge\">\n            <Divider size=\"small\">\n              <Typography.Text className=\"text-xs font-normal\" type=\"secondary\">\n                {t(\"workflow_node.apply.form_anchor.challenge.title\")}\n              </Typography.Text>\n            </Divider>\n\n            <Form.Item\n              name=\"challengeType\"\n              dependencies={[\"identifier\", \"domains\", \"ipaddrs\"]}\n              label={t(\"workflow_node.apply.form.challenge_type.label\")}\n              rules={[formRule]}\n              tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.challenge_type.tooltip\") }}></span>}\n            >\n              <Radio.Group block onChange={(e) => handleChallengeTypeChange(e.target.value)}>\n                <Radio.Button disabled={fieldIdentifier === IDENTIFIER_IP} value={CHALLENGE_TYPE_DNS01}>\n                  DNS-01\n                </Radio.Button>\n                <Radio.Button value={CHALLENGE_TYPE_HTTP01}>HTTP-01</Radio.Button>\n              </Radio.Group>\n            </Form.Item>\n\n            <Form.Item\n              name=\"provider\"\n              dependencies={[\"challengeType\"]}\n              label={\n                fieldChallengeType === CHALLENGE_TYPE_DNS01\n                  ? t(\"workflow_node.apply.form.provider_dns01.label\")\n                  : fieldChallengeType === CHALLENGE_TYPE_HTTP01\n                    ? t(\"workflow_node.apply.form.provider_http01.label\")\n                    : t(\"workflow_node.apply.form.provider.label\")\n              }\n              rules={[formRule]}\n            >\n              {fieldChallengeType === CHALLENGE_TYPE_DNS01 ? (\n                <ACMEDns01ProviderSelect\n                  placeholder={t(\"workflow_node.apply.form.provider_dns01.placeholder\")}\n                  showAvailability\n                  showSearch\n                  onSelect={handleProviderSelect}\n                  onClear={handleProviderSelect}\n                />\n              ) : fieldChallengeType === CHALLENGE_TYPE_HTTP01 ? (\n                <ACMEHttp01ProviderSelect\n                  placeholder={t(\"workflow_node.apply.form.provider_http01.placeholder\")}\n                  showAvailability\n                  showSearch\n                  onSelect={handleProviderSelect}\n                  onClear={handleProviderSelect}\n                />\n              ) : (\n                <Select disabled placeholder={t(\"workflow_node.apply.form.provider.placeholder\")} />\n              )}\n            </Form.Item>\n\n            <Form.Item\n              className=\"relative\"\n              hidden={!showProviderAccess}\n              label={\n                fieldChallengeType === CHALLENGE_TYPE_DNS01\n                  ? t(\"workflow_node.apply.form.provider_access_dns01.label\")\n                  : fieldChallengeType === CHALLENGE_TYPE_HTTP01\n                    ? t(\"workflow_node.apply.form.provider_access_http01.label\")\n                    : t(\"workflow_node.apply.form.provider_access.label\")\n              }\n            >\n              <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n                <AccessEditDrawer\n                  mode=\"create\"\n                  trigger={\n                    <Button size=\"small\" type=\"link\">\n                      {t(\"workflow_node.apply.form.provider_access.button\")}\n                      <IconPlus size=\"1.25em\" />\n                    </Button>\n                  }\n                  usage={fieldChallengeType === CHALLENGE_TYPE_DNS01 ? \"dns\" : fieldChallengeType === CHALLENGE_TYPE_HTTP01 ? \"hosting\" : \"dns-hosting\"}\n                  afterSubmit={(record) => {\n                    if (!accessOptionFilter(record.provider, record)) return;\n                    if (fieldChallengeType === CHALLENGE_TYPE_DNS01 && acmeDns01ProvidersMap.get(fieldProvider!)?.provider !== record.provider) return;\n                    if (fieldChallengeType === CHALLENGE_TYPE_HTTP01 && acmeHttp01ProvidersMap.get(fieldProvider!)?.provider !== record.provider) return;\n                    formInst.setFieldValue(\"providerAccessId\", record.id);\n                  }}\n                />\n              </div>\n              <Form.Item name=\"providerAccessId\" dependencies={[\"challengeType\", \"provider\"]} rules={[formRule]} noStyle>\n                <AccessSelect\n                  disabled={!fieldProvider}\n                  placeholder={\n                    fieldChallengeType === CHALLENGE_TYPE_DNS01\n                      ? t(\"workflow_node.apply.form.provider_access_dns01.placeholder\")\n                      : fieldChallengeType === CHALLENGE_TYPE_HTTP01\n                        ? t(\"workflow_node.apply.form.provider_access_http01.placeholder\")\n                        : t(\"workflow_node.apply.form.provider_access.placeholder\")\n                  }\n                  showSearch\n                  onFilter={accessOptionFilter}\n                />\n              </Form.Item>\n            </Form.Item>\n\n            <FormNestedFieldsContextProvider value={{ parentNamePath: \"providerConfig\" }}>\n              {renderNestedFieldProviderComponent && <>{renderNestedFieldProviderComponent}</>}\n            </FormNestedFieldsContextProvider>\n          </div>\n\n          <div id=\"certificate\" data-anchor=\"certificate\">\n            <Divider size=\"small\">\n              <Typography.Text className=\"text-xs font-normal\" type=\"secondary\">\n                {t(\"workflow_node.apply.form_anchor.certificate.title\")}\n              </Typography.Text>\n            </Divider>\n\n            <Form.Item name=\"keySource\" label={t(\"workflow_node.apply.form.key_source.label\")} rules={[formRule]}>\n              <Radio.Group block onChange={(e) => handleKeySourceChange(e.target.value)}>\n                <Radio.Button value={KEY_SOURCE_AUTO}>{t(\"workflow_node.apply.form.key_source.option.auto.label\")}</Radio.Button>\n                <Radio.Button value={KEY_SOURCE_REUSE}>{t(\"workflow_node.apply.form.key_source.option.reuse.label\")}</Radio.Button>\n                <Radio.Button value={KEY_SOURCE_CUSTOM}>{t(\"workflow_node.apply.form.key_source.option.custom.label\")}</Radio.Button>\n              </Radio.Group>\n            </Form.Item>\n\n            <Form.Item\n              name=\"keyAlgorithm\"\n              label={t(\"workflow_node.apply.form.key_algorithm.label\")}\n              extra={\n                fieldKeySource === KEY_SOURCE_REUSE ? (\n                  <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.key_algorithm.help_reuse\") }}></span>\n                ) : fieldKeySource === KEY_SOURCE_CUSTOM ? (\n                  <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.key_algorithm.help_custom\") }}></span>\n                ) : (\n                  void 0\n                )\n              }\n              rules={[formRule]}\n            >\n              <Select\n                options={[\"RSA2048\", \"RSA3072\", \"RSA4096\", \"RSA8192\", \"EC256\", \"EC384\"].map((e) => ({\n                  label: e,\n                  value: e,\n                }))}\n                placeholder={t(\"workflow_node.apply.form.key_algorithm.placeholder\")}\n              />\n            </Form.Item>\n\n            <Show when={fieldKeySource === KEY_SOURCE_CUSTOM}>\n              <Form.Item name=\"keyContent\" label={t(\"workflow_node.apply.form.key_content.label\")} rules={[formRule]}>\n                <FileTextInput autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t(\"workflow_node.apply.form.key_content.placeholder\")} />\n              </Form.Item>\n            </Show>\n\n            <Form.Item className=\"relative\" label={t(\"workflow_node.apply.form.ca_provider.label\")}>\n              <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n                <Show when={!fieldCAProvider}>\n                  <Link className=\"ant-typography\" to=\"/settings/ssl-provider\" target=\"_blank\">\n                    <Button size=\"small\" type=\"link\">\n                      {t(\"workflow_node.apply.form.ca_provider.button\")}\n                      <IconChevronRight size=\"1.25em\" />\n                    </Button>\n                  </Link>\n                </Show>\n              </div>\n              <Form.Item name=\"caProvider\" noStyle rules={[formRule]}>\n                <CAProviderSelect\n                  allowClear\n                  placeholder={t(\"workflow_node.apply.form.ca_provider.placeholder\")}\n                  showAvailability\n                  showDefault\n                  showSearch\n                  onSelect={handleCAProviderSelect}\n                  onClear={handleCAProviderSelect}\n                />\n              </Form.Item>\n            </Form.Item>\n\n            <Form.Item label={t(\"workflow_node.apply.form.ca_provider_access.label\")} hidden={!showCAProviderAccess}>\n              <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n                <AccessEditDrawer\n                  data={{ provider: caProvidersMap.get(fieldCAProvider!)?.provider }}\n                  mode=\"create\"\n                  trigger={\n                    <Button size=\"small\" type=\"link\">\n                      {t(\"workflow_node.apply.form.ca_provider_access.button\")}\n                      <IconChevronRight size=\"1.25em\" />\n                    </Button>\n                  }\n                  usage=\"ca\"\n                  afterSubmit={(record) => {\n                    if (accessOptionFilterForCA(record.provider, record)) return;\n                    if (caProvidersMap.get(fieldProvider!)?.provider !== record.provider) return;\n                    formInst.setFieldValue(\"caProviderAccessId\", record.id);\n                  }}\n                />\n              </div>\n              <Form.Item name=\"caProviderAccessId\" dependencies={[\"caProvider\"]} noStyle rules={[formRule]}>\n                <AccessSelect\n                  disabled={!fieldCAProvider}\n                  placeholder={t(\"workflow_node.apply.form.ca_provider_access.placeholder\")}\n                  showSearch\n                  onFilter={accessOptionFilterForCA}\n                />\n              </Form.Item>\n            </Form.Item>\n\n            <Form.Item\n              name=\"validityLifetime\"\n              label={t(\"workflow_node.apply.form.validity_lifetime.label\")}\n              extra={t(\"workflow_node.apply.form.validity_lifetime.help\")}\n              rules={[formRule]}\n              tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.validity_lifetime.tooltip\") }}></span>}\n            >\n              <InternalValidityLifetimeInput />\n            </Form.Item>\n\n            <Form.Item\n              name=\"preferredChain\"\n              label={t(\"workflow_node.apply.form.preferred_chain.label\")}\n              extra={t(\"workflow_node.apply.form.preferred_chain.help\")}\n              rules={[formRule]}\n              tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.preferred_chain.tooltip\") }}></span>}\n            >\n              <AutoComplete\n                allowClear\n                options={[\n                  {\n                    ca: \"Let's Encrypt\",\n                    roots: [\"ISRG\", \"ISRG Root X1\", \"ISRG Root X2\"],\n                  },\n                  {\n                    ca: \"Google Trust Services\",\n                    roots: [\"GTS\", \"GTS Root R1\", \"GTS Root R2\", \"GTS Root R3\", \"GTS Root R4\", \"GlobalSign\", \"GlobalSign R4\"],\n                  },\n                ].map((e) => ({\n                  label: e.ca,\n                  options: e.roots.map((s) => ({\n                    label: s,\n                    value: s,\n                  })),\n                }))}\n                placeholder={t(\"workflow_node.apply.form.preferred_chain.placeholder\")}\n                showSearch={{\n                  filterOption: (inputValue, option) => matchSearchOption(inputValue, option!),\n                }}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"acmeProfile\"\n              label={t(\"workflow_node.apply.form.acme_profile.label\")}\n              extra={t(\"workflow_node.apply.form.acme_profile.help\")}\n              rules={[formRule]}\n              tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.acme_profile.tooltip\") }}></span>}\n            >\n              <AutoComplete\n                allowClear\n                options={[\n                  {\n                    ca: \"Let's Encrypt\",\n                    profiles: [\"classic\", \"tlsserver\", \"shortlived\"],\n                  },\n                ].map((e) => ({\n                  label: e.ca,\n                  options: e.profiles.map((s) => ({\n                    label: s,\n                    value: s,\n                  })),\n                }))}\n                placeholder={t(\"workflow_node.apply.form.acme_profile.placeholder\")}\n                showSearch={{\n                  filterOption: (inputValue, option) => matchSearchOption(inputValue, option!),\n                }}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"disableCommonName\"\n              label={t(\"workflow_node.apply.form.disable_cn.label\")}\n              rules={[formRule]}\n              tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.disable_cn.tooltip\") }}></span>}\n            >\n              <Switch />\n            </Form.Item>\n          </div>\n\n          <div id=\"advanced\" data-anchor=\"advanced\">\n            <Divider size=\"small\">\n              <Typography.Text className=\"text-xs font-normal\" type=\"secondary\">\n                {t(\"workflow_node.apply.form_anchor.advanced.title\")}\n              </Typography.Text>\n            </Divider>\n\n            <Form.Item\n              name=\"nameservers\"\n              hidden={fieldIdentifier !== IDENTIFIER_DOMAIN}\n              label={t(\"workflow_node.apply.form.nameservers.label\")}\n              rules={[formRule]}\n              tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.nameservers.tooltip\") }}></span>}\n            >\n              <MultipleSplitValueInput\n                modalTitle={t(\"workflow_node.apply.form.nameservers.multiple_input_modal.title\")}\n                placeholder={t(\"workflow_node.apply.form.nameservers.placeholder\")}\n                placeholderInModal={t(\"workflow_node.apply.form.nameservers.multiple_input_modal.placeholder\")}\n                separator={MULTIPLE_INPUT_SEPARATOR}\n                splitOptions={{ removeEmpty: true, trimSpace: true }}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"dnsPropagationWait\"\n              hidden={fieldChallengeType !== CHALLENGE_TYPE_DNS01}\n              label={t(\"workflow_node.apply.form.dns_propagation_wait.label\")}\n              rules={[formRule]}\n              tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.dns_propagation_wait.tooltip\") }}></span>}\n            >\n              <Input\n                type=\"number\"\n                allowClear\n                min={0}\n                max={3600}\n                placeholder={t(\"workflow_node.apply.form.dns_propagation_wait.placeholder\")}\n                suffix={t(\"workflow_node.apply.form.dns_propagation_wait.unit\")}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"dnsPropagationTimeout\"\n              hidden={fieldChallengeType !== CHALLENGE_TYPE_DNS01}\n              label={t(\"workflow_node.apply.form.dns_propagation_timeout.label\")}\n              rules={[formRule]}\n              tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.dns_propagation_timeout.tooltip\") }}></span>}\n            >\n              <Input\n                type=\"number\"\n                allowClear\n                min={0}\n                max={3600}\n                placeholder={t(\"workflow_node.apply.form.dns_propagation_timeout.placeholder\")}\n                suffix={t(\"workflow_node.apply.form.dns_propagation_timeout.unit\")}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"dnsTTL\"\n              hidden={fieldChallengeType !== CHALLENGE_TYPE_DNS01}\n              label={t(\"workflow_node.apply.form.dns_ttl.label\")}\n              extra={t(\"workflow_node.apply.form.dns_ttl.help\")}\n              rules={[formRule]}\n              tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.dns_ttl.tooltip\") }}></span>}\n            >\n              <Input\n                type=\"number\"\n                allowClear\n                min={0}\n                max={86400}\n                placeholder={t(\"workflow_node.apply.form.dns_ttl.placeholder\")}\n                suffix={t(\"workflow_node.apply.form.dns_ttl.unit\")}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"httpDelayWait\"\n              hidden={fieldChallengeType !== CHALLENGE_TYPE_HTTP01}\n              label={t(\"workflow_node.apply.form.http_delay_wait.label\")}\n              rules={[formRule]}\n              tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.http_delay_wait.tooltip\") }}></span>}\n            >\n              <Input\n                type=\"number\"\n                allowClear\n                min={0}\n                max={3600}\n                placeholder={t(\"workflow_node.apply.form.http_delay_wait.placeholder\")}\n                suffix={t(\"workflow_node.apply.form.http_delay_wait.unit\")}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"disableFollowCNAME\"\n              hidden={fieldChallengeType !== CHALLENGE_TYPE_DNS01}\n              label={t(\"workflow_node.apply.form.disable_follow_cname.label\")}\n              rules={[formRule]}\n              tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.disable_follow_cname.tooltip\") }}></span>}\n            >\n              <Switch />\n            </Form.Item>\n\n            <Form.Item\n              name=\"disableARI\"\n              label={t(\"workflow_node.apply.form.disable_ari.label\")}\n              rules={[formRule]}\n              tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.apply.form.disable_ari.tooltip\") }}></span>}\n            >\n              <Switch />\n            </Form.Item>\n          </div>\n\n          <div id=\"strategy\" data-anchor=\"strategy\">\n            <Divider size=\"small\">\n              <Typography.Text className=\"text-xs font-normal\" type=\"secondary\">\n                {t(\"workflow_node.apply.form_anchor.strategy.title\")}\n              </Typography.Text>\n            </Divider>\n\n            <Form.Item label={t(\"workflow_node.apply.form.skip_before_expiry_days.label\")}>\n              <span className=\"me-2 inline-block\">{t(\"workflow_node.apply.form.skip_before_expiry_days.prefix\")}</span>\n              <span className=\"inline-block\">\n                <Form.Item name=\"skipBeforeExpiryDays\" noStyle rules={[formRule]}>\n                  <InputNumber\n                    className=\"w-24\"\n                    min={1}\n                    max={365}\n                    placeholder={t(\"workflow_node.apply.form.skip_before_expiry_days.placeholder\")}\n                    suffix={t(\"workflow_node.apply.form.skip_before_expiry_days.unit\")}\n                  />\n                </Form.Item>\n              </span>\n              <span className=\"ms-2 inline-block\">{t(\"workflow_node.apply.form.skip_before_expiry_days.suffix\")}</span>\n            </Form.Item>\n          </div>\n        </div>\n      </Form>\n    </NodeFormContextProvider>\n  );\n};\n\nconst InternalIdentifierPicker = memo(({ disabled, onSelect }: { disabled?: boolean; onSelect?: (value: string) => void }) => {\n  const { t } = useTranslation();\n\n  const [value, setValue] = useState<string>();\n\n  const options = [\n    {\n      value: IDENTIFIER_DOMAIN,\n      label: t(\"workflow_node.apply.form.identifier.option.domain.label\"),\n      description: t(\"workflow_node.apply.form.identifier.option.domain.description\"),\n      icon: <IconWorldWww size=\"2rem\" stroke=\"1.25\" />,\n    },\n    {\n      value: IDENTIFIER_IP,\n      label: t(\"workflow_node.apply.form.identifier.option.ip.label\"),\n      description: t(\"workflow_node.apply.form.identifier.option.ip.description\"),\n      icon: <IconMapPin size=\"2rem\" stroke=\"1.25\" />,\n    },\n  ];\n\n  const handleContinueClick = () => {\n    if (!value) return;\n\n    onSelect?.(value);\n  };\n\n  return (\n    <>\n      <Form.Item label={t(\"workflow_node.apply.form.identifier.label2\")}>\n        <div className=\"flex flex-col gap-2\">\n          {options.map((option) => (\n            <Card\n              className={mergeCls(\"relative overflow-hidden\", { [\"border-primary\"]: value === option.value })}\n              hoverable={!disabled}\n              onClick={() => {\n                if (disabled) return;\n\n                setValue(option.value);\n              }}\n            >\n              <div className=\"flex items-center gap-2\">\n                <div className=\"w-24 text-center\">\n                  <Avatar\n                    style={{\n                      background: \"var(--color-primary)\",\n                    }}\n                    icon={option.icon}\n                    size={36}\n                  />\n                  <div className=\"mt-2 truncate text-sm font-medium\">{option.label}</div>\n                </div>\n                <div className=\"flex-1 text-sm\">\n                  <Typography.Paragraph>\n                    <blockquote dangerouslySetInnerHTML={{ __html: option.description }}></blockquote>\n                  </Typography.Paragraph>\n                </div>\n              </div>\n              {value === option.value && <div className=\"absolute top-0 left-0 size-full bg-primary opacity-20\"></div>}\n            </Card>\n          ))}\n        </div>\n        <div className=\"mt-4 flex items-center justify-end gap-4\">\n          <Button disabled={!value || disabled} icon={<IconArrowRight size=\"1.25em\" />} iconPlacement=\"end\" type=\"primary\" onClick={handleContinueClick}>\n            {t(\"workflow_node.apply.form.identifier.continue.button\")}\n          </Button>\n        </div>\n      </Form.Item>\n    </>\n  );\n});\n\nconst InternalEmailInput = memo(({ disabled, ...props }: { disabled?: boolean; value?: string; onChange?: (value: string) => void }) => {\n  const { t } = useTranslation();\n\n  const { emails, fetchEmails, removeEmail } = useContactEmailsStore();\n  useMount(() => {\n    fetchEmails(false);\n  });\n\n  const [value, setValue] = useControllableValue<string>(props, {\n    valuePropName: \"value\",\n    defaultValuePropName: \"defaultValue\",\n    trigger: \"onChange\",\n  });\n\n  const [inputValue, setInputValue] = useState<string>();\n\n  const renderOptionLabel = (email: string, removable: boolean = false) => (\n    <div className=\"flex items-center gap-2 overflow-hidden\">\n      <span className=\"flex-1 truncate overflow-hidden\">{email}</span>\n      {removable && (\n        <Button\n          color=\"default\"\n          disabled={disabled}\n          icon={<IconCircleMinus size=\"1.25em\" />}\n          size=\"small\"\n          type=\"text\"\n          onClick={(e) => {\n            removeEmail(email);\n            e.stopPropagation();\n          }}\n        />\n      )}\n    </div>\n  );\n\n  const options = useMemo(() => {\n    const temp = emails.map((email) => ({\n      label: renderOptionLabel(email, true),\n      value: email,\n    }));\n\n    if (!!inputValue && temp.every((option) => option.value !== inputValue)) {\n      temp.unshift({\n        label: renderOptionLabel(inputValue),\n        value: inputValue,\n      });\n    }\n\n    return temp;\n  }, [emails, inputValue]);\n\n  const handleChange = (value: string) => {\n    setValue(value);\n  };\n\n  const handleSearch = (value: string) => {\n    setInputValue(value?.trim());\n  };\n\n  return (\n    <AutoComplete\n      backfill\n      defaultValue={value}\n      disabled={disabled}\n      options={options}\n      placeholder={t(\"workflow_node.apply.form.contact_email.placeholder\")}\n      showSearch={{\n        filterOption: true,\n        onSearch: handleSearch,\n      }}\n      value={value}\n      onChange={handleChange}\n    />\n  );\n});\n\nconst InternalValidityLifetimeInput = memo(({ disabled, ...props }: { disabled?: boolean; value?: string; onChange?: (value: string) => void }) => {\n  const { t } = useTranslation();\n\n  const [value, setValue] = useControllableValue<string>(props, {\n    valuePropName: \"value\",\n    defaultValuePropName: \"defaultValue\",\n    trigger: \"onChange\",\n  });\n\n  const parseCombinedValue = (val: string): [string | undefined, string | undefined] => {\n    const match = String(val).match(/^(\\d+)([a-zA-Z]+)$/);\n    if (match) {\n      return [match[1], match[2]];\n    }\n\n    return [undefined, undefined];\n  };\n\n  const [inputValue, setInputValue] = useState(parseCombinedValue(value)[0]);\n  const [selectValue, setSelectValue] = useState(parseCombinedValue(value)[1] || \"d\");\n  useEffect(() => {\n    const [v, u] = parseCombinedValue(value);\n    setInputValue(v);\n    setSelectValue(u || \"d\");\n  }, [value]);\n\n  const handleInputClear = () => {\n    setValue(\"\");\n  };\n\n  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setInputValue(e.currentTarget.value);\n\n    if (e.currentTarget.value) {\n      setValue(`${e.currentTarget.value}${selectValue}`);\n    } else {\n      setValue(\"\");\n    }\n  };\n\n  const handleSelectChange = (value: string) => {\n    setSelectValue(value);\n\n    if (inputValue) {\n      setValue(`${inputValue}${value}`);\n    }\n  };\n\n  return (\n    <Space.Compact className=\"w-full\">\n      <Input\n        allowClear\n        disabled={disabled}\n        placeholder={t(\"workflow_node.apply.form.validity_lifetime.placeholder\")}\n        type=\"number\"\n        value={inputValue}\n        onChange={handleInputChange}\n        onClear={handleInputClear}\n      />\n      <div className=\"w-24\">\n        <Select\n          options={[\"h\", \"d\"].map((s) => ({\n            key: s,\n            label: t(`workflow_node.apply.form.validity_lifetime.units.${s}`),\n            value: s,\n          }))}\n          value={selectValue}\n          onChange={handleSelectChange}\n        />\n      </div>\n    </Space.Compact>\n  );\n});\n\nconst getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }): Required<AnchorProps>[\"items\"] => {\n  const { t } = i18n;\n\n  return [\"parameters\", \"challenge\", \"certificate\", \"advanced\", \"strategy\"].map((key) => ({\n    key: key,\n    title: t(`workflow_node.apply.form_anchor.${key}.tab`),\n    href: \"#\" + key,\n  }));\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    contactEmail: \"\",\n    ...(defaultNodeConfigForBizApply() as Nullish<z.infer<ReturnType<typeof getSchema>>>),\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      identifier: z.enum([IDENTIFIER_DOMAIN, IDENTIFIER_IP]),\n      domains: z\n        .string()\n        .nullish()\n        .refine((v) => {\n          if (!v) return true;\n          return v.split(MULTIPLE_INPUT_SEPARATOR).every((e) => isDomain(e, { allowWildcard: true }));\n        }, t(\"common.errmsg.domain_invalid\")),\n      ipaddrs: z\n        .string()\n        .nullish()\n        .refine((v) => {\n          if (!v) return true;\n          return v.split(MULTIPLE_INPUT_SEPARATOR).every((e) => isIPv4(e) || isIPv6(e));\n        }, t(\"common.errmsg.ip_invalid\")),\n      contactEmail: z.email(t(\"common.errmsg.email_invalid\")),\n      challengeType: z.enum([CHALLENGE_TYPE_DNS01, CHALLENGE_TYPE_HTTP01], t(\"workflow_node.apply.form.challenge_type.placeholder\")),\n      provider: z.string().nonempty(t(\"workflow_node.apply.form.provider.placeholder\")),\n      providerAccessId: z.string().nullish(),\n      providerConfig: z.any().nullish(),\n      caProvider: z.string().nullish(),\n      caProviderAccessId: z.string().nullish(),\n      caProviderConfig: z.any().nullish(),\n      keySource: z.enum([KEY_SOURCE_AUTO, KEY_SOURCE_REUSE, KEY_SOURCE_CUSTOM], t(\"workflow_node.apply.form.key_source.placeholder\")),\n      keyAlgorithm: z.string().nonempty(t(\"workflow_node.apply.form.key_algorithm.placeholder\")),\n      keyContent: z.string().nullish(),\n      validityLifetime: z\n        .string()\n        .nullish()\n        .refine((v) => {\n          if (!v) return true;\n          return /^\\d+[d|h]$/.test(v) && parseInt(v) > 0;\n        }, t(\"workflow_node.apply.form.validity_lifetime.placeholder\")),\n      preferredChain: z.string().nullish(),\n      acmeProfile: z.string().nullish(),\n      nameservers: z\n        .string()\n        .nullish()\n        .refine((v) => {\n          if (!v) return true;\n          return v.split(MULTIPLE_INPUT_SEPARATOR).every((e) => isHostname(e) || isDomain(e));\n        }, t(\"common.errmsg.host_invalid\")),\n      dnsPropagationWait: z.preprocess(\n        (v) => (v == null || v === \"\" ? void 0 : Number(v)),\n        z.number().int().gte(0, t(\"workflow_node.apply.form.dns_propagation_wait.placeholder\")).nullish()\n      ),\n      dnsPropagationTimeout: z.preprocess(\n        (v) => (v == null || v === \"\" ? void 0 : Number(v)),\n        z.number().int().gte(1, t(\"workflow_node.apply.form.dns_propagation_timeout.placeholder\")).nullish()\n      ),\n      dnsTTL: z.preprocess(\n        (v) => (v == null || v === \"\" ? void 0 : Number(v)),\n        z.number().int().gte(1, t(\"workflow_node.apply.form.dns_ttl.placeholder\")).nullish()\n      ),\n      httpDelayWait: z.preprocess(\n        (v) => (v == null || v === \"\" ? void 0 : Number(v)),\n        z.number().int().gte(0, t(\"workflow_node.apply.form.http_delay_wait.placeholder\")).nullish()\n      ),\n      disableCommonName: z.boolean().nullish(),\n      disableFollowCNAME: z.boolean().nullish(),\n      disableARI: z.boolean().nullish(),\n      skipBeforeExpiryDays: z.coerce.number().int().positive(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.identifier) {\n        switch (values.identifier) {\n          case IDENTIFIER_DOMAIN:\n            {\n              if (!values.domains) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domains\"],\n                });\n              }\n            }\n            break;\n          case IDENTIFIER_IP:\n            {\n              if (!values.ipaddrs) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.ip_invalid\"),\n                  path: [\"ipaddrs\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n\n      if (values.challengeType) {\n        switch (values.challengeType) {\n          case CHALLENGE_TYPE_DNS01:\n            {\n              if (values.ipaddrs) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"workflow_node.apply.form.challenge_type.errmsg.no_ip_in_dns01\"),\n                  path: [\"challengeType\"],\n                });\n              }\n            }\n            break;\n\n          case CHALLENGE_TYPE_HTTP01:\n            {\n              if (values.domains && values.domains.includes(\"*\")) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"workflow_node.apply.form.challenge_type.errmsg.no_wildcard_in_http01\"),\n                  path: [\"challengeType\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n\n      if (values.keySource) {\n        switch (values.keySource) {\n          case KEY_SOURCE_CUSTOM:\n            {\n              if (!validatePEMPrivateKey(values.keyContent!)) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"workflow_node.apply.form.key_content.errmsg.invalid\"),\n                  path: [\"keyContent\"],\n                });\n              } else {\n                const { algorithm, keySize } = getPKIXPrivateKeyAlgorithm(values.keyContent!);\n                const expectedKeyAlg = values.keyAlgorithm;\n                const actualKeyAlg = `${algorithm}${keySize}`;\n                if (actualKeyAlg !== expectedKeyAlg) {\n                  ctx.addIssue({\n                    code: \"custom\",\n                    message: t(\"workflow_node.apply.form.key_content.errmsg.not_matched\", { expected: expectedKeyAlg, actual: actualKeyAlg }),\n                    path: [\"keyContent\"],\n                  });\n                }\n              }\n            }\n            break;\n        }\n      }\n\n      if (values.provider) {\n        switch (values.challengeType) {\n          case CHALLENGE_TYPE_DNS01:\n            {\n              const provider = acmeDns01ProvidersMap.get(values.provider);\n              if (!provider?.builtin && !values.providerAccessId) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"workflow_node.deploy.form.provider_access.placeholder\"),\n                  path: [\"providerAccessId\"],\n                });\n              }\n            }\n            break;\n\n          case CHALLENGE_TYPE_HTTP01:\n            {\n              const provider = acmeHttp01ProvidersMap.get(values.provider);\n              if (!provider?.builtin && !values.providerAccessId) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"workflow_node.deploy.form.provider_access.placeholder\"),\n                  path: [\"providerAccessId\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n\n      if (values.caProvider) {\n        const provider = caProvidersMap.get(values.caProvider);\n        if (!provider?.builtin && !values.caProviderAccessId) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: t(\"workflow_node.apply.form.ca_provider_access.placeholder\"),\n            path: [\"caProviderAccessId\"],\n          });\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizApplyNodeConfigForm, {\n  getAnchorItems,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigDrawer.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { Form } from \"antd\";\n\nimport { type WorkflowNodeConfigForBizDeploy } from \"@/domain/workflow\";\n\nimport { NodeConfigDrawer } from \"./_shared\";\nimport BizDeployNodeConfigForm from \"./BizDeployNodeConfigForm\";\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface BizDeployNodeConfigDrawerProps {\n  afterClose?: () => void;\n  loading?: boolean;\n  node: FlowNodeEntity;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst BizDeployNodeConfigDrawer = ({ node, ...props }: BizDeployNodeConfigDrawerProps) => {\n  if (node.flowNodeType !== NodeType.BizDeploy) {\n    console.warn(`[certimate] current workflow node type is not: ${NodeType.BizDeploy}`);\n  }\n\n  const { i18n } = useTranslation();\n\n  const [formInst] = Form.useForm();\n\n  const fieldProvider = Form.useWatch<WorkflowNodeConfigForBizDeploy[\"provider\"]>(\"provider\", { form: formInst, preserve: true });\n\n  return (\n    <NodeConfigDrawer\n      anchor={fieldProvider ? { items: BizDeployNodeConfigForm.getAnchorItems({ i18n }) } : false}\n      footer={fieldProvider ? void 0 : false}\n      form={formInst}\n      node={node}\n      {...props}\n    >\n      <BizDeployNodeConfigForm form={formInst} node={node} />\n    </NodeConfigDrawer>\n  );\n};\n\nexport default BizDeployNodeConfigDrawer;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx",
    "content": "﻿import { useEffect, useState } from \"react\";\r\n\r\nimport { DEPLOYMENT_PROVIDERS, type DeploymentProviderType } from \"@/domain/provider\";\r\n\r\nimport BizDeployNodeConfigFieldsProvider1Panel from \"./BizDeployNodeConfigFieldsProvider1Panel.tsx\";\r\nimport BizDeployNodeConfigFieldsProvider1PanelConsole from \"./BizDeployNodeConfigFieldsProvider1PanelConsole\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunALB from \"./BizDeployNodeConfigFieldsProviderAliyunALB\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunAPIGW from \"./BizDeployNodeConfigFieldsProviderAliyunAPIGW\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunCAS from \"./BizDeployNodeConfigFieldsProviderAliyunCAS\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunCASDeploy from \"./BizDeployNodeConfigFieldsProviderAliyunCASDeploy\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunCDN from \"./BizDeployNodeConfigFieldsProviderAliyunCDN\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunCLB from \"./BizDeployNodeConfigFieldsProviderAliyunCLB\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunDCDN from \"./BizDeployNodeConfigFieldsProviderAliyunDCDN\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunDDoSPro from \"./BizDeployNodeConfigFieldsProviderAliyunDDoSPro\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunESA from \"./BizDeployNodeConfigFieldsProviderAliyunESA\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunESASaaS from \"./BizDeployNodeConfigFieldsProviderAliyunESASaaS\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunFC from \"./BizDeployNodeConfigFieldsProviderAliyunFC\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunGA from \"./BizDeployNodeConfigFieldsProviderAliyunGA\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunLive from \"./BizDeployNodeConfigFieldsProviderAliyunLive\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunNLB from \"./BizDeployNodeConfigFieldsProviderAliyunNLB\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunOSS from \"./BizDeployNodeConfigFieldsProviderAliyunOSS\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunVOD from \"./BizDeployNodeConfigFieldsProviderAliyunVOD\";\r\nimport BizDeployNodeConfigFieldsProviderAliyunWAF from \"./BizDeployNodeConfigFieldsProviderAliyunWAF\";\r\nimport BizDeployNodeConfigFieldsProviderAPISIX from \"./BizDeployNodeConfigFieldsProviderAPISIX\";\r\nimport BizDeployNodeConfigFieldsProviderAWSACM from \"./BizDeployNodeConfigFieldsProviderAWSACM\";\r\nimport BizDeployNodeConfigFieldsProviderAWSCloudFront from \"./BizDeployNodeConfigFieldsProviderAWSCloudFront\";\r\nimport BizDeployNodeConfigFieldsProviderAWSIAM from \"./BizDeployNodeConfigFieldsProviderAWSIAM\";\r\nimport BizDeployNodeConfigFieldsProviderAzureKeyVault from \"./BizDeployNodeConfigFieldsProviderAzureKeyVault\";\r\nimport BizDeployNodeConfigFieldsProviderBaiduCloudAppBLB from \"./BizDeployNodeConfigFieldsProviderBaiduCloudAppBLB\";\r\nimport BizDeployNodeConfigFieldsProviderBaiduCloudBLB from \"./BizDeployNodeConfigFieldsProviderBaiduCloudBLB\";\r\nimport BizDeployNodeConfigFieldsProviderBaiduCloudCDN from \"./BizDeployNodeConfigFieldsProviderBaiduCloudCDN\";\r\nimport BizDeployNodeConfigFieldsProviderBaishanCDN from \"./BizDeployNodeConfigFieldsProviderBaishanCDN\";\r\nimport BizDeployNodeConfigFieldsProviderBaotaPanel from \"./BizDeployNodeConfigFieldsProviderBaotaPanel.tsx\";\r\nimport BizDeployNodeConfigFieldsProviderBaotaPanelConsole from \"./BizDeployNodeConfigFieldsProviderBaotaPanelConsole\";\r\nimport BizDeployNodeConfigFieldsProviderBaotaPanelGo from \"./BizDeployNodeConfigFieldsProviderBaotaPanelGo.tsx\";\r\nimport BizDeployNodeConfigFieldsProviderBaotaPanelGoConsole from \"./BizDeployNodeConfigFieldsProviderBaotaPanelGoConsole\";\r\nimport BizDeployNodeConfigFieldsProviderBaotaWAF from \"./BizDeployNodeConfigFieldsProviderBaotaWAF.tsx\";\r\nimport BizDeployNodeConfigFieldsProviderBaotaWAFConsole from \"./BizDeployNodeConfigFieldsProviderBaotaWAFConsole.tsx\";\r\nimport BizDeployNodeConfigFieldsProviderBunnyCDN from \"./BizDeployNodeConfigFieldsProviderBunnyCDN\";\r\nimport BizDeployNodeConfigFieldsProviderBytePlusCDN from \"./BizDeployNodeConfigFieldsProviderBytePlusCDN\";\r\nimport BizDeployNodeConfigFieldsProviderCdnfly from \"./BizDeployNodeConfigFieldsProviderCdnfly\";\r\nimport BizDeployNodeConfigFieldsProviderCPanel from \"./BizDeployNodeConfigFieldsProviderCPanel.tsx\";\r\nimport BizDeployNodeConfigFieldsProviderCTCCCloudAO from \"./BizDeployNodeConfigFieldsProviderCTCCCloudAO\";\r\nimport BizDeployNodeConfigFieldsProviderCTCCCloudCDN from \"./BizDeployNodeConfigFieldsProviderCTCCCloudCDN\";\r\nimport BizDeployNodeConfigFieldsProviderCTCCCloudELB from \"./BizDeployNodeConfigFieldsProviderCTCCCloudELB\";\r\nimport BizDeployNodeConfigFieldsProviderCTCCCloudFaaS from \"./BizDeployNodeConfigFieldsProviderCTCCCloudFaaS\";\r\nimport BizDeployNodeConfigFieldsProviderCTCCCloudICDN from \"./BizDeployNodeConfigFieldsProviderCTCCCloudICDN\";\r\nimport BizDeployNodeConfigFieldsProviderCTCCCloudLVDN from \"./BizDeployNodeConfigFieldsProviderCTCCCloudLVDN\";\r\nimport BizDeployNodeConfigFieldsProviderDogeCloudCDN from \"./BizDeployNodeConfigFieldsProviderDogeCloudCDN\";\r\nimport BizDeployNodeConfigFieldsProviderFlexCDN from \"./BizDeployNodeConfigFieldsProviderFlexCDN\";\r\nimport BizDeployNodeConfigFieldsProviderFlyIO from \"./BizDeployNodeConfigFieldsProviderFlyIO\";\r\nimport BizDeployNodeConfigFieldsProviderGcoreCDN from \"./BizDeployNodeConfigFieldsProviderGcoreCDN\";\r\nimport BizDeployNodeConfigFieldsProviderGoEdge from \"./BizDeployNodeConfigFieldsProviderGoEdge\";\r\nimport BizDeployNodeConfigFieldsProviderHuaweiCloudCDN from \"./BizDeployNodeConfigFieldsProviderHuaweiCloudCDN\";\r\nimport BizDeployNodeConfigFieldsProviderHuaweiCloudELB from \"./BizDeployNodeConfigFieldsProviderHuaweiCloudELB\";\r\nimport BizDeployNodeConfigFieldsProviderHuaweiCloudOBS from \"./BizDeployNodeConfigFieldsProviderHuaweiCloudOBS\";\r\nimport BizDeployNodeConfigFieldsProviderHuaweiCloudWAF from \"./BizDeployNodeConfigFieldsProviderHuaweiCloudWAF\";\r\nimport BizDeployNodeConfigFieldsProviderJDCloudALB from \"./BizDeployNodeConfigFieldsProviderJDCloudALB\";\r\nimport BizDeployNodeConfigFieldsProviderJDCloudCDN from \"./BizDeployNodeConfigFieldsProviderJDCloudCDN\";\r\nimport BizDeployNodeConfigFieldsProviderJDCloudLive from \"./BizDeployNodeConfigFieldsProviderJDCloudLive\";\r\nimport BizDeployNodeConfigFieldsProviderJDCloudVOD from \"./BizDeployNodeConfigFieldsProviderJDCloudVOD\";\r\nimport BizDeployNodeConfigFieldsProviderKong from \"./BizDeployNodeConfigFieldsProviderKong\";\r\nimport BizDeployNodeConfigFieldsProviderKsyunCDN from \"./BizDeployNodeConfigFieldsProviderKsyunCDN\";\r\nimport BizDeployNodeConfigFieldsProviderKubernetesSecret from \"./BizDeployNodeConfigFieldsProviderKubernetesSecret\";\r\nimport BizDeployNodeConfigFieldsProviderLeCDN from \"./BizDeployNodeConfigFieldsProviderLeCDN\";\r\nimport BizDeployNodeConfigFieldsProviderLocal from \"./BizDeployNodeConfigFieldsProviderLocal\";\r\nimport BizDeployNodeConfigFieldsProviderMohuaMVH from \"./BizDeployNodeConfigFieldsProviderMohuaMVH\";\r\nimport BizDeployNodeConfigFieldsProviderNetlify from \"./BizDeployNodeConfigFieldsProviderNetlify.tsx\";\r\nimport BizDeployNodeConfigFieldsProviderNginxProxyManager from \"./BizDeployNodeConfigFieldsProviderNginxProxyManager\";\r\nimport BizDeployNodeConfigFieldsProviderProxmoxVE from \"./BizDeployNodeConfigFieldsProviderProxmoxVE\";\r\nimport BizDeployNodeConfigFieldsProviderQiniuCDN from \"./BizDeployNodeConfigFieldsProviderQiniuCDN\";\r\nimport BizDeployNodeConfigFieldsProviderQiniuKodo from \"./BizDeployNodeConfigFieldsProviderQiniuKodo\";\r\nimport BizDeployNodeConfigFieldsProviderQiniuPili from \"./BizDeployNodeConfigFieldsProviderQiniuPili\";\r\nimport BizDeployNodeConfigFieldsProviderRainYunRCDN from \"./BizDeployNodeConfigFieldsProviderRainYunRCDN\";\r\nimport BizDeployNodeConfigFieldsProviderRainYunSSLCenter from \"./BizDeployNodeConfigFieldsProviderRainYunSSLCenter\";\r\nimport BizDeployNodeConfigFieldsProviderRatPanel from \"./BizDeployNodeConfigFieldsProviderRatPanel.tsx\";\r\nimport BizDeployNodeConfigFieldsProviderS3 from \"./BizDeployNodeConfigFieldsProviderS3\";\r\nimport BizDeployNodeConfigFieldsProviderSafeLine from \"./BizDeployNodeConfigFieldsProviderSafeLine.tsx\";\r\nimport BizDeployNodeConfigFieldsProviderSSH from \"./BizDeployNodeConfigFieldsProviderSSH\";\r\nimport BizDeployNodeConfigFieldsProviderSynologyDSM from \"./BizDeployNodeConfigFieldsProviderSynologyDSM\";\r\nimport BizDeployNodeConfigFieldsProviderTencentCloudCDN from \"./BizDeployNodeConfigFieldsProviderTencentCloudCDN\";\r\nimport BizDeployNodeConfigFieldsProviderTencentCloudCLB from \"./BizDeployNodeConfigFieldsProviderTencentCloudCLB\";\r\nimport BizDeployNodeConfigFieldsProviderTencentCloudCOS from \"./BizDeployNodeConfigFieldsProviderTencentCloudCOS\";\r\nimport BizDeployNodeConfigFieldsProviderTencentCloudCSS from \"./BizDeployNodeConfigFieldsProviderTencentCloudCSS\";\r\nimport BizDeployNodeConfigFieldsProviderTencentCloudECDN from \"./BizDeployNodeConfigFieldsProviderTencentCloudECDN\";\r\nimport BizDeployNodeConfigFieldsProviderTencentCloudEO from \"./BizDeployNodeConfigFieldsProviderTencentCloudEO\";\r\nimport BizDeployNodeConfigFieldsProviderTencentCloudGAAP from \"./BizDeployNodeConfigFieldsProviderTencentCloudGAAP\";\r\nimport BizDeployNodeConfigFieldsProviderTencentCloudSCF from \"./BizDeployNodeConfigFieldsProviderTencentCloudSCF\";\r\nimport BizDeployNodeConfigFieldsProviderTencentCloudSSL from \"./BizDeployNodeConfigFieldsProviderTencentCloudSSL\";\r\nimport BizDeployNodeConfigFieldsProviderTencentCloudSSLDeploy from \"./BizDeployNodeConfigFieldsProviderTencentCloudSSLDeploy\";\r\nimport BizDeployNodeConfigFieldsProviderTencentCloudSSLUpdate from \"./BizDeployNodeConfigFieldsProviderTencentCloudSSLUpdate\";\r\nimport BizDeployNodeConfigFieldsProviderTencentCloudVOD from \"./BizDeployNodeConfigFieldsProviderTencentCloudVOD\";\r\nimport BizDeployNodeConfigFieldsProviderTencentCloudWAF from \"./BizDeployNodeConfigFieldsProviderTencentCloudWAF\";\r\nimport BizDeployNodeConfigFieldsProviderUCloudUALB from \"./BizDeployNodeConfigFieldsProviderUCloudUALB\";\r\nimport BizDeployNodeConfigFieldsProviderUCloudUCDN from \"./BizDeployNodeConfigFieldsProviderUCloudUCDN\";\r\nimport BizDeployNodeConfigFieldsProviderUCloudUCLB from \"./BizDeployNodeConfigFieldsProviderUCloudUCLB\";\r\nimport BizDeployNodeConfigFieldsProviderUCloudUEWAF from \"./BizDeployNodeConfigFieldsProviderUCloudUEWAF\";\r\nimport BizDeployNodeConfigFieldsProviderUCloudUPathX from \"./BizDeployNodeConfigFieldsProviderUCloudUPathX\";\r\nimport BizDeployNodeConfigFieldsProviderUCloudUS3 from \"./BizDeployNodeConfigFieldsProviderUCloudUS3\";\r\nimport BizDeployNodeConfigFieldsProviderUniCloudWebHost from \"./BizDeployNodeConfigFieldsProviderUniCloudWebHost\";\r\nimport BizDeployNodeConfigFieldsProviderUpyunCDN from \"./BizDeployNodeConfigFieldsProviderUpyunCDN\";\r\nimport BizDeployNodeConfigFieldsProviderUpyunFile from \"./BizDeployNodeConfigFieldsProviderUpyunFile\";\r\nimport BizDeployNodeConfigFieldsProviderVolcEngineALB from \"./BizDeployNodeConfigFieldsProviderVolcEngineALB\";\r\nimport BizDeployNodeConfigFieldsProviderVolcEngineCDN from \"./BizDeployNodeConfigFieldsProviderVolcEngineCDN\";\r\nimport BizDeployNodeConfigFieldsProviderVolcEngineCertCenter from \"./BizDeployNodeConfigFieldsProviderVolcEngineCertCenter\";\r\nimport BizDeployNodeConfigFieldsProviderVolcEngineCLB from \"./BizDeployNodeConfigFieldsProviderVolcEngineCLB\";\r\nimport BizDeployNodeConfigFieldsProviderVolcEngineDCDN from \"./BizDeployNodeConfigFieldsProviderVolcEngineDCDN\";\r\nimport BizDeployNodeConfigFieldsProviderVolcEngineImageX from \"./BizDeployNodeConfigFieldsProviderVolcEngineImageX\";\r\nimport BizDeployNodeConfigFieldsProviderVolcEngineLive from \"./BizDeployNodeConfigFieldsProviderVolcEngineLive\";\r\nimport BizDeployNodeConfigFieldsProviderVolcEngineTOS from \"./BizDeployNodeConfigFieldsProviderVolcEngineTOS\";\r\nimport BizDeployNodeConfigFieldsProviderVolcEngineVOD from \"./BizDeployNodeConfigFieldsProviderVolcEngineVOD.tsx\";\r\nimport BizDeployNodeConfigFieldsProviderVolcEngineWAF from \"./BizDeployNodeConfigFieldsProviderVolcEngineWAF.tsx\";\r\nimport BizDeployNodeConfigFieldsProviderWangsuCDN from \"./BizDeployNodeConfigFieldsProviderWangsuCDN\";\r\nimport BizDeployNodeConfigFieldsProviderWangsuCDNPro from \"./BizDeployNodeConfigFieldsProviderWangsuCDNPro\";\r\nimport BizDeployNodeConfigFieldsProviderWangsuCertificate from \"./BizDeployNodeConfigFieldsProviderWangsuCertificate\";\r\nimport BizDeployNodeConfigFieldsProviderWebhook from \"./BizDeployNodeConfigFieldsProviderWebhook\";\r\n\r\nconst providerComponentMap: Partial<Record<DeploymentProviderType, React.ComponentType<any>>> = {\r\n  /*\r\n    注意：如果追加新的子组件，请保持以 ASCII 排序。\r\n    NOTICE: If you add new child component, please keep ASCII order.\r\n    */\r\n  [DEPLOYMENT_PROVIDERS[\"1PANEL_CONSOLE\"]]: BizDeployNodeConfigFieldsProvider1PanelConsole,\r\n  [DEPLOYMENT_PROVIDERS[\"1PANEL\"]]: BizDeployNodeConfigFieldsProvider1Panel,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_ALB]: BizDeployNodeConfigFieldsProviderAliyunALB,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_APIGW]: BizDeployNodeConfigFieldsProviderAliyunAPIGW,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_CAS]: BizDeployNodeConfigFieldsProviderAliyunCAS,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_CAS_DEPLOY]: BizDeployNodeConfigFieldsProviderAliyunCASDeploy,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_CLB]: BizDeployNodeConfigFieldsProviderAliyunCLB,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_CDN]: BizDeployNodeConfigFieldsProviderAliyunCDN,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_DCDN]: BizDeployNodeConfigFieldsProviderAliyunDCDN,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_DDOSPRO]: BizDeployNodeConfigFieldsProviderAliyunDDoSPro,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_ESA]: BizDeployNodeConfigFieldsProviderAliyunESA,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_ESA_SAAS]: BizDeployNodeConfigFieldsProviderAliyunESASaaS,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_FC]: BizDeployNodeConfigFieldsProviderAliyunFC,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_GA]: BizDeployNodeConfigFieldsProviderAliyunGA,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_LIVE]: BizDeployNodeConfigFieldsProviderAliyunLive,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_NLB]: BizDeployNodeConfigFieldsProviderAliyunNLB,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_OSS]: BizDeployNodeConfigFieldsProviderAliyunOSS,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_VOD]: BizDeployNodeConfigFieldsProviderAliyunVOD,\r\n  [DEPLOYMENT_PROVIDERS.ALIYUN_WAF]: BizDeployNodeConfigFieldsProviderAliyunWAF,\r\n  [DEPLOYMENT_PROVIDERS.APISIX]: BizDeployNodeConfigFieldsProviderAPISIX,\r\n  [DEPLOYMENT_PROVIDERS.AWS_ACM]: BizDeployNodeConfigFieldsProviderAWSACM,\r\n  [DEPLOYMENT_PROVIDERS.AWS_CLOUDFRONT]: BizDeployNodeConfigFieldsProviderAWSCloudFront,\r\n  [DEPLOYMENT_PROVIDERS.AWS_IAM]: BizDeployNodeConfigFieldsProviderAWSIAM,\r\n  [DEPLOYMENT_PROVIDERS.AZURE_KEYVAULT]: BizDeployNodeConfigFieldsProviderAzureKeyVault,\r\n  [DEPLOYMENT_PROVIDERS.BAIDUCLOUD_APPBLB]: BizDeployNodeConfigFieldsProviderBaiduCloudAppBLB,\r\n  [DEPLOYMENT_PROVIDERS.BAIDUCLOUD_BLB]: BizDeployNodeConfigFieldsProviderBaiduCloudBLB,\r\n  [DEPLOYMENT_PROVIDERS.BAIDUCLOUD_CDN]: BizDeployNodeConfigFieldsProviderBaiduCloudCDN,\r\n  [DEPLOYMENT_PROVIDERS.BAISHAN_CDN]: BizDeployNodeConfigFieldsProviderBaishanCDN,\r\n  [DEPLOYMENT_PROVIDERS.BAOTAPANEL_CONSOLE]: BizDeployNodeConfigFieldsProviderBaotaPanelConsole,\r\n  [DEPLOYMENT_PROVIDERS.BAOTAPANEL]: BizDeployNodeConfigFieldsProviderBaotaPanel,\r\n  [DEPLOYMENT_PROVIDERS.BAOTAPANELGO_CONSOLE]: BizDeployNodeConfigFieldsProviderBaotaPanelGoConsole,\r\n  [DEPLOYMENT_PROVIDERS.BAOTAPANELGO]: BizDeployNodeConfigFieldsProviderBaotaPanelGo,\r\n  [DEPLOYMENT_PROVIDERS.BAOTAWAF]: BizDeployNodeConfigFieldsProviderBaotaWAF,\r\n  [DEPLOYMENT_PROVIDERS.BAOTAWAF_CONSOLE]: BizDeployNodeConfigFieldsProviderBaotaWAFConsole,\r\n  [DEPLOYMENT_PROVIDERS.BUNNY_CDN]: BizDeployNodeConfigFieldsProviderBunnyCDN,\r\n  [DEPLOYMENT_PROVIDERS.BYTEPLUS_CDN]: BizDeployNodeConfigFieldsProviderBytePlusCDN,\r\n  [DEPLOYMENT_PROVIDERS.CDNFLY]: BizDeployNodeConfigFieldsProviderCdnfly,\r\n  [DEPLOYMENT_PROVIDERS.CPANEL]: BizDeployNodeConfigFieldsProviderCPanel,\r\n  [DEPLOYMENT_PROVIDERS.CTCCCLOUD_AO]: BizDeployNodeConfigFieldsProviderCTCCCloudAO,\r\n  [DEPLOYMENT_PROVIDERS.CTCCCLOUD_CDN]: BizDeployNodeConfigFieldsProviderCTCCCloudCDN,\r\n  [DEPLOYMENT_PROVIDERS.CTCCCLOUD_ELB]: BizDeployNodeConfigFieldsProviderCTCCCloudELB,\r\n  [DEPLOYMENT_PROVIDERS.CTCCCLOUD_FAAS]: BizDeployNodeConfigFieldsProviderCTCCCloudFaaS,\r\n  [DEPLOYMENT_PROVIDERS.CTCCCLOUD_ICDN]: BizDeployNodeConfigFieldsProviderCTCCCloudICDN,\r\n  [DEPLOYMENT_PROVIDERS.CTCCCLOUD_LVDN]: BizDeployNodeConfigFieldsProviderCTCCCloudLVDN,\r\n  [DEPLOYMENT_PROVIDERS.DOGECLOUD_CDN]: BizDeployNodeConfigFieldsProviderDogeCloudCDN,\r\n  [DEPLOYMENT_PROVIDERS.FLEXCDN]: BizDeployNodeConfigFieldsProviderFlexCDN,\r\n  [DEPLOYMENT_PROVIDERS.FLYIO]: BizDeployNodeConfigFieldsProviderFlyIO,\r\n  [DEPLOYMENT_PROVIDERS.GCORE_CDN]: BizDeployNodeConfigFieldsProviderGcoreCDN,\r\n  [DEPLOYMENT_PROVIDERS.GOEDGE]: BizDeployNodeConfigFieldsProviderGoEdge,\r\n  [DEPLOYMENT_PROVIDERS.HUAWEICLOUD_CDN]: BizDeployNodeConfigFieldsProviderHuaweiCloudCDN,\r\n  [DEPLOYMENT_PROVIDERS.HUAWEICLOUD_ELB]: BizDeployNodeConfigFieldsProviderHuaweiCloudELB,\r\n  [DEPLOYMENT_PROVIDERS.HUAWEICLOUD_OBS]: BizDeployNodeConfigFieldsProviderHuaweiCloudOBS,\r\n  [DEPLOYMENT_PROVIDERS.HUAWEICLOUD_WAF]: BizDeployNodeConfigFieldsProviderHuaweiCloudWAF,\r\n  [DEPLOYMENT_PROVIDERS.JDCLOUD_ALB]: BizDeployNodeConfigFieldsProviderJDCloudALB,\r\n  [DEPLOYMENT_PROVIDERS.JDCLOUD_CDN]: BizDeployNodeConfigFieldsProviderJDCloudCDN,\r\n  [DEPLOYMENT_PROVIDERS.JDCLOUD_LIVE]: BizDeployNodeConfigFieldsProviderJDCloudLive,\r\n  [DEPLOYMENT_PROVIDERS.JDCLOUD_VOD]: BizDeployNodeConfigFieldsProviderJDCloudVOD,\r\n  [DEPLOYMENT_PROVIDERS.KONG]: BizDeployNodeConfigFieldsProviderKong,\r\n  [DEPLOYMENT_PROVIDERS.KUBERNETES_SECRET]: BizDeployNodeConfigFieldsProviderKubernetesSecret,\r\n  [DEPLOYMENT_PROVIDERS.KSYUN_CDN]: BizDeployNodeConfigFieldsProviderKsyunCDN,\r\n  [DEPLOYMENT_PROVIDERS.LECDN]: BizDeployNodeConfigFieldsProviderLeCDN,\r\n  [DEPLOYMENT_PROVIDERS.LOCAL]: BizDeployNodeConfigFieldsProviderLocal,\r\n  [DEPLOYMENT_PROVIDERS.MOHUA_MVH]: BizDeployNodeConfigFieldsProviderMohuaMVH,\r\n  [DEPLOYMENT_PROVIDERS.NETLIFY]: BizDeployNodeConfigFieldsProviderNetlify,\r\n  [DEPLOYMENT_PROVIDERS.NGINXPROXYMANAGER]: BizDeployNodeConfigFieldsProviderNginxProxyManager,\r\n  [DEPLOYMENT_PROVIDERS.PROXMOXVE]: BizDeployNodeConfigFieldsProviderProxmoxVE,\r\n  [DEPLOYMENT_PROVIDERS.QINIU_CDN]: BizDeployNodeConfigFieldsProviderQiniuCDN,\r\n  [DEPLOYMENT_PROVIDERS.QINIU_KODO]: BizDeployNodeConfigFieldsProviderQiniuKodo,\r\n  [DEPLOYMENT_PROVIDERS.QINIU_PILI]: BizDeployNodeConfigFieldsProviderQiniuPili,\r\n  [DEPLOYMENT_PROVIDERS.RAINYUN_RCDN]: BizDeployNodeConfigFieldsProviderRainYunRCDN,\r\n  [DEPLOYMENT_PROVIDERS.RAINYUN_SSLCENTER]: BizDeployNodeConfigFieldsProviderRainYunSSLCenter,\r\n  [DEPLOYMENT_PROVIDERS.RATPANEL]: BizDeployNodeConfigFieldsProviderRatPanel,\r\n  [DEPLOYMENT_PROVIDERS.S3]: BizDeployNodeConfigFieldsProviderS3,\r\n  [DEPLOYMENT_PROVIDERS.SAFELINE]: BizDeployNodeConfigFieldsProviderSafeLine,\r\n  [DEPLOYMENT_PROVIDERS.SSH]: BizDeployNodeConfigFieldsProviderSSH,\r\n  [DEPLOYMENT_PROVIDERS.SYNOLOGYDSM]: BizDeployNodeConfigFieldsProviderSynologyDSM,\r\n  [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CDN]: BizDeployNodeConfigFieldsProviderTencentCloudCDN,\r\n  [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CLB]: BizDeployNodeConfigFieldsProviderTencentCloudCLB,\r\n  [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_COS]: BizDeployNodeConfigFieldsProviderTencentCloudCOS,\r\n  [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CSS]: BizDeployNodeConfigFieldsProviderTencentCloudCSS,\r\n  [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_ECDN]: BizDeployNodeConfigFieldsProviderTencentCloudECDN,\r\n  [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_EO]: BizDeployNodeConfigFieldsProviderTencentCloudEO,\r\n  [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_GAAP]: BizDeployNodeConfigFieldsProviderTencentCloudGAAP,\r\n  [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SCF]: BizDeployNodeConfigFieldsProviderTencentCloudSCF,\r\n  [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SSL]: BizDeployNodeConfigFieldsProviderTencentCloudSSL,\r\n  [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SSL_DEPLOY]: BizDeployNodeConfigFieldsProviderTencentCloudSSLDeploy,\r\n  [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SSL_UPDATE]: BizDeployNodeConfigFieldsProviderTencentCloudSSLUpdate,\r\n  [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_VOD]: BizDeployNodeConfigFieldsProviderTencentCloudVOD,\r\n  [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_WAF]: BizDeployNodeConfigFieldsProviderTencentCloudWAF,\r\n  [DEPLOYMENT_PROVIDERS.UCLOUD_UALB]: BizDeployNodeConfigFieldsProviderUCloudUALB,\r\n  [DEPLOYMENT_PROVIDERS.UCLOUD_UCDN]: BizDeployNodeConfigFieldsProviderUCloudUCDN,\r\n  [DEPLOYMENT_PROVIDERS.UCLOUD_UCLB]: BizDeployNodeConfigFieldsProviderUCloudUCLB,\r\n  [DEPLOYMENT_PROVIDERS.UCLOUD_UEWAF]: BizDeployNodeConfigFieldsProviderUCloudUEWAF,\r\n  [DEPLOYMENT_PROVIDERS.UCLOUD_UPATHX]: BizDeployNodeConfigFieldsProviderUCloudUPathX,\r\n  [DEPLOYMENT_PROVIDERS.UCLOUD_US3]: BizDeployNodeConfigFieldsProviderUCloudUS3,\r\n  [DEPLOYMENT_PROVIDERS.UNICLOUD_WEBHOST]: BizDeployNodeConfigFieldsProviderUniCloudWebHost,\r\n  [DEPLOYMENT_PROVIDERS.UPYUN_CDN]: BizDeployNodeConfigFieldsProviderUpyunCDN,\r\n  [DEPLOYMENT_PROVIDERS.UPYUN_FILE]: BizDeployNodeConfigFieldsProviderUpyunFile,\r\n  [DEPLOYMENT_PROVIDERS.VOLCENGINE_ALB]: BizDeployNodeConfigFieldsProviderVolcEngineALB,\r\n  [DEPLOYMENT_PROVIDERS.VOLCENGINE_CDN]: BizDeployNodeConfigFieldsProviderVolcEngineCDN,\r\n  [DEPLOYMENT_PROVIDERS.VOLCENGINE_CERTCENTER]: BizDeployNodeConfigFieldsProviderVolcEngineCertCenter,\r\n  [DEPLOYMENT_PROVIDERS.VOLCENGINE_CLB]: BizDeployNodeConfigFieldsProviderVolcEngineCLB,\r\n  [DEPLOYMENT_PROVIDERS.VOLCENGINE_DCDN]: BizDeployNodeConfigFieldsProviderVolcEngineDCDN,\r\n  [DEPLOYMENT_PROVIDERS.VOLCENGINE_IMAGEX]: BizDeployNodeConfigFieldsProviderVolcEngineImageX,\r\n  [DEPLOYMENT_PROVIDERS.VOLCENGINE_LIVE]: BizDeployNodeConfigFieldsProviderVolcEngineLive,\r\n  [DEPLOYMENT_PROVIDERS.VOLCENGINE_TOS]: BizDeployNodeConfigFieldsProviderVolcEngineTOS,\r\n  [DEPLOYMENT_PROVIDERS.VOLCENGINE_VOD]: BizDeployNodeConfigFieldsProviderVolcEngineVOD,\r\n  [DEPLOYMENT_PROVIDERS.VOLCENGINE_WAF]: BizDeployNodeConfigFieldsProviderVolcEngineWAF,\r\n  [DEPLOYMENT_PROVIDERS.WANGSU_CDN]: BizDeployNodeConfigFieldsProviderWangsuCDN,\r\n  [DEPLOYMENT_PROVIDERS.WANGSU_CDNPRO]: BizDeployNodeConfigFieldsProviderWangsuCDNPro,\r\n  [DEPLOYMENT_PROVIDERS.WANGSU_CERTIFICATE]: BizDeployNodeConfigFieldsProviderWangsuCertificate,\r\n  [DEPLOYMENT_PROVIDERS.WEBHOOK]: BizDeployNodeConfigFieldsProviderWebhook,\r\n};\r\n\r\nconst useComponent = (provider: string, { initProps, deps = [] }: { initProps?: (provider: string) => any; deps?: unknown[] }) => {\r\n  const initComponent = () => {\r\n    const Component = providerComponentMap[provider as DeploymentProviderType];\r\n    if (!Component) return null;\r\n\r\n    const props = initProps?.(provider);\r\n    if (props) {\r\n      return <Component {...props} />;\r\n    }\r\n\r\n    return <Component />;\r\n  };\r\n\r\n  const [component, setComponent] = useState(() => initComponent());\r\n\r\n  useEffect(() => setComponent(initComponent()), [provider]);\r\n  useEffect(() => setComponent(initComponent()), deps);\r\n\r\n  return component;\r\n};\r\n\r\nconst _default = {\r\n  useComponent,\r\n};\r\n\r\nexport default _default;\r\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider1Panel.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_WEBSITE = \"website\" as const;\nconst RESOURCE_TYPE_CERTIFICATE = \"certificate\" as const;\n\nconst WEBSITE_MATCH_PATTERN_SPECIFIED = \"specified\" as const;\nconst WEBSITE_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProvider1Panel = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n  const fieldWebsiteMatchPattern = Form.useWatch([parentNamePath, \"websiteMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"nodeName\"]}\n        initialValue={initialValues.nodeName}\n        label={t(\"workflow_node.deploy.form.1panel_node_name.label\")}\n        extra={t(\"workflow_node.deploy.form.1panel_node_name.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.1panel_node_name.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.1panel_node_name.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_WEBSITE, RESOURCE_TYPE_CERTIFICATE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.1panel_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_WEBSITE}>\n        <Form.Item\n          name={[parentNamePath, \"websiteMatchPattern\"]}\n          initialValue={initialValues.websiteMatchPattern}\n          label={t(\"workflow_node.deploy.form.1panel_website_match_pattern.label\")}\n          extra={\n            fieldWebsiteMatchPattern === WEBSITE_MATCH_PATTERN_CERTSAN ? (\n              <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.1panel_website_match_pattern.help_certsan\") }}></span>\n            ) : (\n              void 0\n            )\n          }\n          rules={[formRule]}\n        >\n          <Radio.Group\n            options={[WEBSITE_MATCH_PATTERN_SPECIFIED, WEBSITE_MATCH_PATTERN_CERTSAN].map((s) => ({\n              key: s,\n              label: t(`workflow_node.deploy.form.1panel_website_match_pattern.option.${s}.label`),\n              value: s,\n            }))}\n          />\n        </Form.Item>\n\n        <Show when={fieldWebsiteMatchPattern !== WEBSITE_MATCH_PATTERN_CERTSAN}>\n          <Form.Item\n            name={[parentNamePath, \"websiteId\"]}\n            initialValue={initialValues.websiteId}\n            label={t(\"workflow_node.deploy.form.1panel_website_id.label\")}\n            rules={[formRule]}\n            tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.1panel_website_id.tooltip\") }}></span>}\n          >\n            <Input type=\"number\" placeholder={t(\"workflow_node.deploy.form.1panel_website_id.placeholder\")} />\n          </Form.Item>\n        </Show>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>\n        <Form.Item\n          name={[parentNamePath, \"certificateId\"]}\n          initialValue={initialValues.certificateId}\n          label={t(\"workflow_node.deploy.form.1panel_certificate_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.1panel_certificate_id.tooltip\") }}></span>}\n        >\n          <Input type=\"number\" placeholder={t(\"workflow_node.deploy.form.1panel_certificate_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceType: RESOURCE_TYPE_WEBSITE,\n    websiteMatchPattern: WEBSITE_MATCH_PATTERN_SPECIFIED,\n    websiteId: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      nodeName: z.string().nullish(),\n      resourceType: z.literal([RESOURCE_TYPE_WEBSITE, RESOURCE_TYPE_CERTIFICATE], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      websiteMatchPattern: z.string().nullish(),\n      websiteId: z.union([z.string(), z.number().int()]).nullish(),\n      certificateId: z.union([z.string(), z.number().int()]).nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_WEBSITE:\n          {\n            if (values.websiteMatchPattern) {\n              switch (values.websiteMatchPattern) {\n                case WEBSITE_MATCH_PATTERN_SPECIFIED:\n                  {\n                    const scWebsiteId = z.coerce.number().int().positive();\n                    if (!scWebsiteId.safeParse(values.websiteId).success) {\n                      ctx.addIssue({\n                        code: \"custom\",\n                        message: t(\"workflow_node.deploy.form.1panel_website_id.placeholder\"),\n                        path: [\"websiteId\"],\n                      });\n                    }\n                  }\n                  break;\n              }\n            } else {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.1panel_website_match_pattern.placeholder\"),\n                path: [\"websiteMatchPattern\"],\n              });\n            }\n          }\n          break;\n\n        case RESOURCE_TYPE_CERTIFICATE:\n          {\n            const scCertificateId = z.coerce.number().int().positive();\n            if (!scCertificateId.safeParse(values.certificateId).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.1panel_certificate_id.placeholder\"),\n                path: [\"websiteId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProvider1Panel, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider1PanelConsole.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProvider1PanelConsole = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"autoRestart\"]}\n        initialValue={initialValues.autoRestart}\n        label={t(\"workflow_node.deploy.form.1panel_console_auto_restart.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    autoRestart: true,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t: _ } = i18n;\n\n  return z.object({\n    autoRestart: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProvider1PanelConsole, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAPISIX.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_CERTIFICATE = \"certificate\" as const;\n\nconst BizDeployNodeConfigFieldsProviderAPISIX = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.apisix.guide\") }}></span>} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_CERTIFICATE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.apisix_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>\n        <Form.Item\n          name={[parentNamePath, \"certificateId\"]}\n          initialValue={initialValues.certificateId}\n          label={t(\"workflow_node.deploy.form.apisix_certificate_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.apisix_certificate_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.apisix_certificate_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceType: RESOURCE_TYPE_CERTIFICATE,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      certificateId: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_CERTIFICATE:\n          {\n            if (!values.certificateId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.apisix_certificate_id.placeholder\"),\n                path: [\"certificateId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAPISIX, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAWSACM.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderAWSACM = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aws_acm_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aws_acm_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aws_acm_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"certificateArn\"]}\n        initialValue={initialValues.certificateArn}\n        label={t(\"workflow_node.deploy.form.aws_acm_certificate_arn.label\")}\n        extra={t(\"workflow_node.deploy.form.aws_acm_certificate_arn.help\")}\n        rules={[formRule]}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.aws_acm_certificate_arn.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.deploy.form.aws_acm_region.placeholder\")),\n    certificateArn: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAWSACM, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAWSCloudFront.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderAWSCloudFront = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aws_cloudfront_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aws_cloudfront_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aws_cloudfront_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"distributionId\"]}\n        initialValue={initialValues.distributionId}\n        label={t(\"workflow_node.deploy.form.aws_cloudfront_distribution_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aws_cloudfront_distribution_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aws_cloudfront_distribution_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"certificateSource\"]}\n        initialValue={initialValues.certificateSource}\n        label={t(\"workflow_node.deploy.form.aws_cloudfront_certificate_source.label\")}\n        rules={[formRule]}\n      >\n        <Select placeholder={t(\"workflow_node.deploy.form.aws_cloudfront_certificate_source.placeholder\")}>\n          <Select.Option key=\"ACM\" value=\"ACM\">\n            ACM\n          </Select.Option>\n          <Select.Option key=\"IAM\" value=\"IAM\">\n            IAM\n          </Select.Option>\n        </Select>\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    certificateSource: \"ACM\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.deploy.form.aws_cloudfront_region.placeholder\")),\n    distributionId: z.string().nonempty(t(\"workflow_node.deploy.form.aws_cloudfront_distribution_id.placeholder\")),\n    certificateSource: z.string().nonempty(t(\"workflow_node.deploy.form.aws_cloudfront_certificate_source.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAWSCloudFront, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAWSIAM.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderAWSIAM = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aws_iam_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aws_iam_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aws_iam_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"certificatePath\"]}\n        initialValue={initialValues.certificatePath}\n        label={t(\"workflow_node.deploy.form.aws_iam_certificate_path.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aws_iam_certificate_path.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.aws_iam_certificate_path.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    certificatePath: \"/\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.deploy.form.aws_iam_region.placeholder\")),\n    certificatePath: z\n      .string()\n      .nullish()\n      .refine((v) => {\n        if (!v) return true;\n        return v.startsWith(\"/\") && v.endsWith(\"/\");\n      }, t(\"workflow_node.deploy.form.aws_iam_certificate_path.errmsg.invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAWSIAM, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunALB.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_LOADBALANCER = \"loadbalancer\" as const;\nconst RESOURCE_TYPE_LISTENER = \"listener\" as const;\n\nconst BizDeployNodeConfigFieldsProviderAliyunALB = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aliyun_alb_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_alb_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_alb_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.aliyun_alb_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER}>\n        <Form.Item\n          name={[parentNamePath, \"loadbalancerId\"]}\n          initialValue={initialValues.loadbalancerId}\n          label={t(\"workflow_node.deploy.form.aliyun_alb_loadbalancer_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_alb_loadbalancer_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.aliyun_alb_loadbalancer_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"listenerId\"]}\n          initialValue={initialValues.listenerId}\n          label={t(\"workflow_node.deploy.form.aliyun_alb_listener_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_alb_listener_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.aliyun_alb_listener_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER || fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.aliyun_alb_snidomain.label\")}\n          extra={t(\"workflow_node.deploy.form.aliyun_alb_snidomain.help\")}\n          rules={[formRule]}\n        >\n          <Input allowClear placeholder={t(\"workflow_node.deploy.form.aliyun_alb_snidomain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    resourceType: RESOURCE_TYPE_LISTENER,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_alb_region.placeholder\")),\n      resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      loadbalancerId: z.string().nullish(),\n      listenerId: z.string().nullish(),\n      domain: z\n        .string()\n        .nullish()\n        .refine((v) => {\n          if (!v) return true;\n          return isDomain(v, { allowWildcard: true });\n        }, t(\"common.errmsg.domain_invalid\")),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_LOADBALANCER:\n          {\n            const scLoadbalancerId = z.string().nonempty();\n            if (!scLoadbalancerId.safeParse(values.loadbalancerId).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.aliyun_alb_loadbalancer_id.placeholder\"),\n                path: [\"loadbalancerId\"],\n              });\n            }\n          }\n          break;\n\n        case RESOURCE_TYPE_LISTENER:\n          {\n            const scListenerId = z.string().nonempty();\n            if (!scListenerId.safeParse(values.listenerId).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.aliyun_alb_listener_id.placeholder\"),\n                path: [\"listenerId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunALB, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunAPIGW.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst SERVICE_TYPE_CLOUDNATIVE = \"cloudnative\" as const;\nconst SERVICE_TYPE_TRADITIONAL = \"traditional\" as const;\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderAliyunAPIGW = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldServiceType = Form.useWatch([parentNamePath, \"serviceType\"], formInst);\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aliyun_apigw_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_apigw_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_apigw_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"serviceType\"]}\n        initialValue={initialValues.serviceType}\n        label={t(\"workflow_node.deploy.form.aliyun_apigw_service_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[SERVICE_TYPE_CLOUDNATIVE, SERVICE_TYPE_CLOUDNATIVE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.aliyun_apigw_service_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.aliyun_apigw_service_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldServiceType === SERVICE_TYPE_CLOUDNATIVE}>\n        <Form.Item\n          name={[parentNamePath, \"gatewayId\"]}\n          initialValue={initialValues.gatewayId}\n          label={t(\"workflow_node.deploy.form.aliyun_apigw_gateway_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_apigw_gateway_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.aliyun_apigw_gateway_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldServiceType === SERVICE_TYPE_TRADITIONAL}>\n        <Form.Item\n          name={[parentNamePath, \"groupId\"]}\n          initialValue={initialValues.groupId}\n          label={t(\"workflow_node.deploy.form.aliyun_apigw_group_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_apigw_group_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.aliyun_apigw_group_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.aliyun_apigw_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.aliyun_apigw_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      serviceType: z.literal([SERVICE_TYPE_CLOUDNATIVE, SERVICE_TYPE_TRADITIONAL], t(\"workflow_node.deploy.form.aliyun_apigw_service_type.placeholder\")),\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_apigw_region.placeholder\")),\n      gatewayId: z.string().nullish(),\n      groupId: z.string().nullish(),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.serviceType) {\n        switch (values.serviceType) {\n          case SERVICE_TYPE_CLOUDNATIVE:\n            {\n              if (!values.gatewayId?.trim()) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"workflow_node.deploy.form.aliyun_apigw_gateway_id.placeholder\"),\n                  path: [\"gatewayId\"],\n                });\n              }\n            }\n            break;\n\n          case SERVICE_TYPE_TRADITIONAL:\n            {\n              if (!values.groupId?.trim()) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"workflow_node.deploy.form.aliyun_apigw_group_id.placeholder\"),\n                  path: [\"groupId\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunAPIGW, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunCAS.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderAliyunCAS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aliyun_cas_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_cas_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_cas_region.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_cas_region.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunCAS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunCASDeploy.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport MultipleSplitValueInput from \"@/components/MultipleSplitValueInput\";\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst MULTIPLE_INPUT_SEPARATOR = \";\";\n\nconst BizDeployNodeConfigFieldsProviderAliyunCASDeploy = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_casdeploy.guide\") }}></span>} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aliyun_casdeploy_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_casdeploy_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_casdeploy_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceIds\"]}\n        initialValue={initialValues.resourceIds}\n        label={t(\"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.label\")}\n        extra={t(\"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.tooltip\") }}></span>}\n      >\n        <MultipleSplitValueInput\n          modalTitle={t(\"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.multiple_input_modal.title\")}\n          placeholder={t(\"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.placeholder\")}\n          placeholderInModal={t(\"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.multiple_input_modal.placeholder\")}\n          separator={MULTIPLE_INPUT_SEPARATOR}\n          splitOptions={{ removeEmpty: true, trimSpace: true }}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"contactIds\"]}\n        initialValue={initialValues.contactIds}\n        label={t(\"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.label\")}\n        extra={t(\"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.tooltip\") }}></span>}\n      >\n        <MultipleSplitValueInput\n          modalTitle={t(\"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.multiple_input_modal.title\")}\n          placeholder={t(\"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.placeholder\")}\n          placeholderInModal={t(\"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.multiple_input_modal.placeholder\")}\n          separator={MULTIPLE_INPUT_SEPARATOR}\n          splitOptions={{ removeEmpty: true, trimSpace: true }}\n        />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    resourceIds: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_casdeploy_region.placeholder\")),\n    resourceIds: z.string().refine((v) => {\n      if (!v) return false;\n      return v.split(MULTIPLE_INPUT_SEPARATOR).every((e) => /^[1-9]\\d*$/.test(e));\n    }, t(\"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.errmsg.invalid\")),\n    contactIds: z\n      .string()\n      .nullish()\n      .refine((v) => {\n        if (!v) return true;\n        return v.split(MULTIPLE_INPUT_SEPARATOR).every((e) => /^[1-9]\\d*$/.test(e));\n      }, t(\"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.errmsg.invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunCASDeploy, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { AutoComplete, Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderAliyunCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aliyun_cdn_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_cdn_region.tooltip\") }}></span>}\n      >\n        <AutoComplete\n          allowClear\n          options={[\"cn-hangzhou\", \"ap-southeast-1\"].map((s) => ({ value: s }))}\n          placeholder={t(\"workflow_node.deploy.form.aliyun_cdn_region.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.aliyun_cdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.aliyun_cdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nullish(),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunCLB.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain, isPortNumber } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_LOADBALANCER = \"loadbalancer\" as const;\nconst RESOURCE_TYPE_LISTENER = \"listener\" as const;\n\nconst BizDeployNodeConfigFieldsProviderAliyunCLB = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aliyun_clb_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_clb_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_clb_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.aliyun_clb_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"loadbalancerId\"]}\n        initialValue={initialValues.loadbalancerId}\n        label={t(\"workflow_node.deploy.form.aliyun_clb_loadbalancer_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_clb_loadbalancer_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_clb_loadbalancer_id.placeholder\")} />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"listenerPort\"]}\n          initialValue={initialValues.listenerPort}\n          label={t(\"workflow_node.deploy.form.aliyun_clb_listener_port.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_clb_listener_port.tooltip\") }}></span>}\n        >\n          <Input type=\"number\" min={1} max={65535} placeholder={t(\"workflow_node.deploy.form.aliyun_clb_listener_port.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER || fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.aliyun_clb_snidomain.label\")}\n          extra={t(\"workflow_node.deploy.form.aliyun_clb_snidomain.help\")}\n          rules={[formRule]}\n        >\n          <Input allowClear placeholder={t(\"workflow_node.deploy.form.aliyun_clb_snidomain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    resourceType: RESOURCE_TYPE_LISTENER,\n    loadbalancerId: \"\",\n    listenerPort: 443,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_clb_region.placeholder\")),\n      resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      loadbalancerId: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_clb_loadbalancer_id.placeholder\")),\n      listenerPort: z.preprocess((v) => (v == null || v === \"\" ? void 0 : Number(v)), z.number().nullish()),\n      domain: z\n        .string()\n        .nullish()\n        .refine((v) => {\n          if (!v) return true;\n          return isDomain(v, { allowWildcard: true });\n        }, t(\"common.errmsg.domain_invalid\")),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_LISTENER:\n          {\n            if (!isPortNumber(values.listenerPort!)) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.aliyun_clb_listener_port.placeholder\"),\n                path: [\"listenerPort\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunCLB, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunDCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { AutoComplete, Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderAliyunDCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aliyun_dcdn_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_dcdn_region.tooltip\") }}></span>}\n      >\n        <AutoComplete\n          allowClear\n          options={[\"cn-hangzhou\", \"ap-southeast-1\"].map((s) => ({ value: s }))}\n          placeholder={t(\"workflow_node.deploy.form.aliyun_dcdn_region.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.aliyun_dcdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.aliyun_dcdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nullish(),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunDCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunDDoSPro.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderAliyunDDoSPro = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aliyun_ddospro_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_ddospro_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_ddospro_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.aliyun_ddospro_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.aliyun_ddospro_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_ddospro_region.placeholder\")),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunDDoSPro, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunESA.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderAliyunESA = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aliyun_esa_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_esa_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_esa_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"siteId\"]}\n        initialValue={initialValues.siteId}\n        label={t(\"workflow_node.deploy.form.aliyun_esa_site_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_esa_site_id.tooltip\") }}></span>}\n      >\n        <Input type=\"number\" placeholder={t(\"workflow_node.deploy.form.aliyun_esa_site_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    siteId: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_esa_region.placeholder\")),\n    siteId: z.union([z.string(), z.number().int()]).refine((v) => {\n      return /^\\d+$/.test(v + \"\") && +v > 0;\n    }, t(\"workflow_node.deploy.form.aliyun_esa_site_id.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunESA, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunESASaaS.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderAliyunESASaaS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aliyun_esa_saas_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_esa_saas_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_esa_saas_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"siteId\"]}\n        initialValue={initialValues.siteId}\n        label={t(\"workflow_node.deploy.form.aliyun_esa_saas_site_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_esa_saas_site_id.tooltip\") }}></span>}\n      >\n        <Input type=\"number\" placeholder={t(\"workflow_node.deploy.form.aliyun_esa_saas_site_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.aliyun_esa_saas_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.aliyun_esa_saas_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    siteId: \"\",\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_esa_saas_region.placeholder\")),\n      siteId: z.union([z.string(), z.number().int()]).refine((v) => {\n        return /^\\d+$/.test(v + \"\") && +v > 0;\n      }, t(\"workflow_node.deploy.form.aliyun_esa_saas_site_id.placeholder\")),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunESASaaS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunFC.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderAliyunFC = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aliyun_fc_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_fc_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_fc_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"serviceVersion\"]}\n        initialValue={initialValues.serviceVersion}\n        label={t(\"workflow_node.deploy.form.aliyun_fc_service_version.label\")}\n        rules={[formRule]}\n      >\n        <Select placeholder={t(\"workflow_node.deploy.form.aliyun_fc_service_version.placeholder\")}>\n          <Select.Option key=\"2.0\" value=\"2.0\">\n            2.0\n          </Select.Option>\n          <Select.Option key=\"3.0\" value=\"3.0\">\n            3.0\n          </Select.Option>\n        </Select>\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.aliyun_fc_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.aliyun_fc_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    serviceVersion: \"3.0\",\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_fc_region.placeholder\")),\n      serviceVersion: z.literal([\"2.0\", \"3.0\"], t(\"workflow_node.deploy.form.aliyun_fc_service_version.placeholder\")),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunFC, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunGA.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_ACCELERATOR = \"accelerator\" as const;\nconst RESOURCE_TYPE_LISTENER = \"listener\" as const;\n\nconst BizDeployNodeConfigFieldsProviderAliyunGA = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_ACCELERATOR, RESOURCE_TYPE_LISTENER].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.aliyun_ga_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"acceleratorId\"]}\n        initialValue={initialValues.acceleratorId}\n        label={t(\"workflow_node.deploy.form.aliyun_ga_accelerator_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_ga_accelerator_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_ga_accelerator_id.placeholder\")} />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"listenerId\"]}\n          initialValue={initialValues.listenerId}\n          label={t(\"workflow_node.deploy.form.aliyun_ga_listener_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_ga_listener_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.aliyun_ga_listener_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_ACCELERATOR || fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.aliyun_ga_snidomain.label\")}\n          extra={t(\"workflow_node.deploy.form.aliyun_ga_snidomain.help\")}\n          rules={[formRule]}\n        >\n          <Input allowClear placeholder={t(\"workflow_node.deploy.form.aliyun_ga_snidomain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceType: RESOURCE_TYPE_LISTENER,\n    acceleratorId: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      resourceType: z.literal([RESOURCE_TYPE_ACCELERATOR, RESOURCE_TYPE_LISTENER], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      acceleratorId: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_ga_accelerator_id.placeholder\")),\n      listenerId: z.string().nullish(),\n      domain: z\n        .string()\n        .nullish()\n        .refine((v) => {\n          if (!v) return true;\n          return isDomain(v);\n        }, t(\"common.errmsg.domain_invalid\")),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_LISTENER:\n          {\n            if (!values.listenerId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.aliyun_ga_listener_id.placeholder\"),\n                path: [\"listenerId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunGA, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunLive.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderAliyunLive = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aliyun_live_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_live_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_live_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.aliyun_live_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.aliyun_live_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_live_region.placeholder\")),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunLive, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunNLB.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_LOADBALANCER = \"loadbalancer\" as const;\nconst RESOURCE_TYPE_LISTENER = \"listener\" as const;\n\nconst BizDeployNodeConfigFieldsProviderAliyunNLB = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aliyun_nlb_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_nlb_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_nlb_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.aliyun_nlb_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER}>\n        <Form.Item\n          name={[parentNamePath, \"loadbalancerId\"]}\n          initialValue={initialValues.loadbalancerId}\n          label={t(\"workflow_node.deploy.form.aliyun_nlb_loadbalancer_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_nlb_loadbalancer_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.aliyun_nlb_loadbalancer_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"listenerId\"]}\n          initialValue={initialValues.listenerId}\n          label={t(\"workflow_node.deploy.form.aliyun_nlb_listener_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_nlb_listener_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.aliyun_nlb_listener_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    resourceType: RESOURCE_TYPE_LISTENER,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_nlb_region.placeholder\")),\n      resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      loadbalancerId: z.string().nullish(),\n      listenerId: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_LOADBALANCER:\n          {\n            if (!values.loadbalancerId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.aliyun_nlb_loadbalancer_id.placeholder\"),\n                path: [\"loadbalancerId\"],\n              });\n            }\n          }\n          break;\n\n        case RESOURCE_TYPE_LISTENER:\n          {\n            if (!values.listenerId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.aliyun_nlb_listener_id.placeholder\"),\n                path: [\"listenerId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunNLB, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunOSS.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderAliyunOSS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aliyun_oss_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_oss_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_oss_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"bucket\"]}\n        initialValue={initialValues.bucket}\n        label={t(\"workflow_node.deploy.form.aliyun_oss_bucket.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_oss_bucket.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.aliyun_oss_domain.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_oss_domain.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    bucket: \"\",\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_oss_region.placeholder\")),\n    bucket: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_oss_bucket.placeholder\")),\n    domain: z.string().refine((v) => isDomain(v, { allowWildcard: true }), t(\"common.errmsg.domain_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunOSS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunVOD.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderAliyunVOD = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aliyun_vod_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_vod_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_vod_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.aliyun_vod_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.aliyun_vod_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_vod_region.placeholder\")),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunVOD, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunWAF.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { AutoComplete, Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { matchSearchOption } from \"@/utils/search\";\nimport { isDomain, isPortNumber } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst SERVICE_TYPE_CLOUDRESOURCE = \"cloudresource\" as const;\nconst SERVICE_TYPE_CNAME = \"cname\" as const;\n\nconst BizDeployNodeConfigFieldsProviderAliyunWAF = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldServiceType = Form.useWatch([parentNamePath, \"serviceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.aliyun_waf_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_waf_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_waf_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"serviceVersion\"]}\n        initialValue={initialValues.serviceVersion}\n        label={t(\"workflow_node.deploy.form.aliyun_waf_service_version.label\")}\n        rules={[formRule]}\n      >\n        <Select placeholder={t(\"workflow_node.deploy.form.aliyun_waf_service_version.placeholder\")}>\n          <Select.Option key=\"3.0\" value=\"3.0\">\n            3.0\n          </Select.Option>\n        </Select>\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"serviceType\"]}\n        initialValue={initialValues.serviceType}\n        label={t(\"workflow_node.deploy.form.aliyun_waf_service_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[SERVICE_TYPE_CLOUDRESOURCE, SERVICE_TYPE_CNAME].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.aliyun_waf_service_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.aliyun_waf_service_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"instanceId\"]}\n        initialValue={initialValues.instanceId}\n        label={t(\"workflow_node.deploy.form.aliyun_waf_instance_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.aliyun_waf_instance_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.aliyun_waf_instance_id.placeholder\")} />\n      </Form.Item>\n\n      <Show when={fieldServiceType === SERVICE_TYPE_CLOUDRESOURCE}>\n        <Form.Item\n          name={[parentNamePath, \"resourceProduct\"]}\n          initialValue={initialValues.resourceProduct}\n          label={t(\"workflow_node.deploy.form.aliyun_waf_resource_product.label\")}\n          rules={[formRule]}\n        >\n          <AutoComplete\n            options={[\"ecs\", \"clb4\", \"clb7\", \"nlb\"].map((value) => ({ value }))}\n            placeholder={t(\"workflow_node.deploy.form.aliyun_waf_resource_product.placeholder\")}\n            showSearch={{\n              filterOption: (inputValue, option) => matchSearchOption(inputValue, option!),\n            }}\n          />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"resourceId\"]}\n          initialValue={initialValues.resourceId}\n          label={t(\"workflow_node.deploy.form.aliyun_waf_resource_id.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.aliyun_waf_resource_id.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"resourcePort\"]}\n          initialValue={initialValues.resourcePort}\n          label={t(\"workflow_node.deploy.form.aliyun_waf_resource_port.label\")}\n          rules={[formRule]}\n        >\n          <Input type=\"number\" min={1} max={65535} placeholder={t(\"workflow_node.deploy.form.aliyun_waf_resource_port.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.aliyun_waf_domain.label\")}\n        extra={t(\"workflow_node.deploy.form.aliyun_waf_domain.help\")}\n        rules={[formRule]}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.aliyun_waf_domain.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    serviceVersion: \"3.0\",\n    instanceId: \"\",\n    resourceProduct: \"\",\n    resourceId: \"\",\n    resourcePort: 443,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_waf_region.placeholder\")),\n      serviceVersion: z.literal(\"3.0\", t(\"workflow_node.deploy.form.aliyun_waf_service_version.placeholder\")),\n      serviceType: z.literal([SERVICE_TYPE_CLOUDRESOURCE, SERVICE_TYPE_CNAME], t(\"workflow_node.deploy.form.aliyun_waf_service_type.placeholder\")),\n      instanceId: z.string().nonempty(t(\"workflow_node.deploy.form.aliyun_waf_instance_id.placeholder\")),\n      resourceProduct: z.string().nullish(),\n      resourceId: z.string().nullish(),\n      resourcePort: z.preprocess((v) => (v == null || v === \"\" ? void 0 : Number(v)), z.number().nullish()),\n      domain: z\n        .string()\n        .nullish()\n        .refine((v) => {\n          if (!v) return true;\n          return isDomain(v, { allowWildcard: true });\n        }, t(\"common.errmsg.domain_invalid\")),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.serviceType) {\n        case SERVICE_TYPE_CLOUDRESOURCE:\n          {\n            if (!values.resourceProduct) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.aliyun_waf_resource_product.placeholder\"),\n                path: [\"resourceProduct\"],\n              });\n            }\n\n            if (!values.resourceId) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.aliyun_waf_resource_id.placeholder\"),\n                path: [\"resourceId\"],\n              });\n            }\n\n            if (!isPortNumber(values.resourcePort!)) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.aliyun_waf_resource_port.placeholder\"),\n                path: [\"resourcePort\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunWAF, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAzureKeyVault.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderAzureKeyVault = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"keyvaultName\"]}\n        initialValue={initialValues.keyvaultName}\n        label={t(\"workflow_node.deploy.form.azure_keyvault_name.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.azure_keyvault_name.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.azure_keyvault_name.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"certificateName\"]}\n        initialValue={initialValues.certificateName}\n        label={t(\"workflow_node.deploy.form.azure_keyvault_certificate_name.label\")}\n        extra={t(\"workflow_node.deploy.form.azure_keyvault_certificate_name.help\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.azure_keyvault_certificate_name.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    keyvaultName: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    keyvaultName: z.string().nonempty(t(\"workflow_node.deploy.form.azure_keyvault_name.placeholder\")),\n    certificateName: z\n      .string()\n      .nullish()\n      .refine((v) => {\n        if (!v) return true;\n        return /^[a-zA-Z0-9-]{1,127}$/.test(v);\n      }, t(\"workflow_node.deploy.form.azure_keyvault_certificate_name.errmsg.invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderAzureKeyVault, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaiduCloudAppBLB.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain, isPortNumber } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_LOADBALANCER = \"loadbalancer\" as const;\nconst RESOURCE_TYPE_LISTENER = \"listener\" as const;\n\nconst BizDeployNodeConfigFieldsProviderBaiduCloudAppBLB = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.baiducloud_appblb_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.baiducloud_appblb_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.baiducloud_appblb_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.baiducloud_appblb_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"loadbalancerId\"]}\n        initialValue={initialValues.loadbalancerId}\n        label={t(\"workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder\")} />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"listenerPort\"]}\n          initialValue={initialValues.listenerPort}\n          label={t(\"workflow_node.deploy.form.baiducloud_appblb_listener_port.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.baiducloud_appblb_listener_port.tooltip\") }}></span>}\n        >\n          <Input type=\"number\" min={1} max={65535} placeholder={t(\"workflow_node.deploy.form.baiducloud_appblb_listener_port.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER || fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.baiducloud_appblb_snidomain.label\")}\n          extra={t(\"workflow_node.deploy.form.baiducloud_appblb_snidomain.help\")}\n          rules={[formRule]}\n        >\n          <Input allowClear placeholder={t(\"workflow_node.deploy.form.baiducloud_appblb_snidomain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    resourceType: RESOURCE_TYPE_LISTENER,\n    loadbalancerId: \"\",\n    listenerPort: 443,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.baiducloud_appblb_region.placeholder\")),\n      resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      loadbalancerId: z.string().nonempty(t(\"workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder\")),\n      listenerPort: z.preprocess((v) => (v == null || v === \"\" ? void 0 : Number(v)), z.number().nullish()),\n      domain: z\n        .string()\n        .nullish()\n        .refine((v) => {\n          if (!v) return true;\n          return isDomain(v, { allowWildcard: true });\n        }, t(\"common.errmsg.domain_invalid\")),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_LISTENER:\n          {\n            if (!isPortNumber(values.listenerPort!)) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.baiducloud_appblb_listener_port.placeholder\"),\n                path: [\"listenerPort\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderBaiduCloudAppBLB, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaiduCloudBLB.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain, isPortNumber } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_LOADBALANCER = \"loadbalancer\" as const;\nconst RESOURCE_TYPE_LISTENER = \"listener\" as const;\n\nconst BizDeployNodeConfigFieldsProviderBaiduCloudBLB = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.baiducloud_blb_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.baiducloud_blb_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.baiducloud_blb_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.baiducloud_blb_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"loadbalancerId\"]}\n        initialValue={initialValues.loadbalancerId}\n        label={t(\"workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder\")} />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"listenerPort\"]}\n          initialValue={initialValues.listenerPort}\n          label={t(\"workflow_node.deploy.form.baiducloud_blb_listener_port.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.baiducloud_blb_listener_port.tooltip\") }}></span>}\n        >\n          <Input type=\"number\" min={1} max={65535} placeholder={t(\"workflow_node.deploy.form.baiducloud_blb_listener_port.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER || fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.baiducloud_blb_snidomain.label\")}\n          extra={t(\"workflow_node.deploy.form.baiducloud_blb_snidomain.help\")}\n          rules={[formRule]}\n        >\n          <Input allowClear placeholder={t(\"workflow_node.deploy.form.baiducloud_blb_snidomain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    resourceType: RESOURCE_TYPE_LISTENER,\n    loadbalancerId: \"\",\n    listenerPort: 443,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.baiducloud_blb_region.placeholder\")),\n      resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      loadbalancerId: z.string().nonempty(t(\"workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder\")),\n      listenerPort: z.preprocess((v) => (v == null || v === \"\" ? void 0 : Number(v)), z.number().nullish()),\n      domain: z\n        .string()\n        .nullish()\n        .refine((v) => {\n          if (!v) return true;\n          return isDomain(v, { allowWildcard: true });\n        }, t(\"common.errmsg.domain_invalid\")),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_LISTENER:\n          {\n            if (!isPortNumber(values.listenerPort!)) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.baiducloud_blb_listener_port.placeholder\"),\n                path: [\"listenerPort\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderBaiduCloudBLB, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaiduCloudCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderBaiduCloudCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.baiducloud_cdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.baiducloud_cdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderBaiduCloudCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaishanCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_DOMAIN = \"domain\" as const;\nconst RESOURCE_TYPE_CERTIFICATE = \"certificate\" as const;\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\n\nconst BizDeployNodeConfigFieldsProviderBaishanCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_DOMAIN, RESOURCE_TYPE_CERTIFICATE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.baishan_cdn_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_DOMAIN}>\n        <Form.Item\n          name={[parentNamePath, \"domainMatchPattern\"]}\n          initialValue={initialValues.domainMatchPattern}\n          label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n          extra={\n            fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n              <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n            ) : (\n              void 0\n            )\n          }\n          rules={[formRule]}\n        >\n          <Radio.Group\n            options={[DOMAIN_MATCH_PATTERN_EXACT].map((s) => ({\n              key: s,\n              label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n              value: s,\n            }))}\n          />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.baishan_cdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.baishan_cdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>\n        <Form.Item\n          name={[parentNamePath, \"certificateId\"]}\n          initialValue={initialValues.certificateId}\n          label={t(\"workflow_node.deploy.form.baishan_cdn_certificate_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.baishan_cdn_certificate_id.tooltip\") }}></span>}\n        >\n          <Input allowClear type=\"number\" placeholder={t(\"workflow_node.deploy.form.baishan_cdn_certificate_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceType: RESOURCE_TYPE_DOMAIN,\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      resourceType: z.literal([RESOURCE_TYPE_DOMAIN, RESOURCE_TYPE_CERTIFICATE], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n      certificateId: z.union([z.string(), z.number().int()]).nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_DOMAIN:\n          {\n            if (!values.domainMatchPattern) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\"),\n                path: [\"domainMatchPattern\"],\n              });\n            }\n\n            switch (values.domainMatchPattern) {\n              case DOMAIN_MATCH_PATTERN_EXACT:\n                {\n                  if (!isDomain(values.domain!, { allowWildcard: true })) {\n                    ctx.addIssue({\n                      code: \"custom\",\n                      message: t(\"common.errmsg.domain_invalid\"),\n                      path: [\"domain\"],\n                    });\n                  }\n                }\n                break;\n            }\n          }\n          break;\n\n        case RESOURCE_TYPE_CERTIFICATE:\n          {\n            if (!values.certificateId) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.baishan_cdn_certificate_id.placeholder\"),\n                path: [\"certificateId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderBaishanCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaotaPanel.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport MultipleSplitValueInput from \"@/components/MultipleSplitValueInput\";\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst MULTIPLE_INPUT_SEPARATOR = \";\";\n\nconst BizDeployNodeConfigFieldsProviderBaotaPanel = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.baotapanel.guide\") }}></span>} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"siteType\"]}\n        initialValue={initialValues.siteType}\n        label={t(\"workflow_node.deploy.form.baotapanel_site_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[\"php\", \"java\", \"nodejs\", \"go\", \"python\", \"proxy\", \"html\", \"general\", \"any\"].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.baotapanel_site_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.baotapanel_site_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"siteNames\"]}\n        initialValue={initialValues.siteNames}\n        label={t(\"workflow_node.deploy.form.baotapanel_site_names.label\")}\n        extra={t(\"workflow_node.deploy.form.baotapanel_site_names.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.baotapanel_site_names.tooltip\") }}></span>}\n      >\n        <MultipleSplitValueInput\n          modalTitle={t(\"workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.title\")}\n          placeholder={t(\"workflow_node.deploy.form.baotapanel_site_names.placeholder\")}\n          placeholderInModal={t(\"workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.placeholder\")}\n          separator={MULTIPLE_INPUT_SEPARATOR}\n          splitOptions={{ removeEmpty: true, trimSpace: true }}\n        />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    siteType: \"any\",\n    siteNames: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    siteType: z.literal([\"php\", \"any\"], t(\"workflow_node.deploy.form.baotapanel_site_type.placeholder\")),\n    siteNames: z\n      .string()\n      .nonempty(t(\"workflow_node.deploy.form.baotapanel_site_names.placeholder\"))\n      .refine(\n        (v) => {\n          if (!v) return false;\n          return v.split(MULTIPLE_INPUT_SEPARATOR).every((s) => !!s.trim());\n        },\n        { error: t(\"workflow_node.deploy.form.baotapanel_site_names.placeholder\") }\n      ),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderBaotaPanel, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaotaPanelConsole.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderBaotaPanelConsole = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.baotapanel_console.guide\") }}></span>} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"autoRestart\"]}\n        initialValue={initialValues.autoRestart}\n        label={t(\"workflow_node.deploy.form.baotapanel_console_auto_restart.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    autoRestart: true,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t: _ } = i18n;\n\n  return z.object({\n    autoRestart: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderBaotaPanelConsole, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaotaPanelGo.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport MultipleSplitValueInput from \"@/components/MultipleSplitValueInput\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst MULTIPLE_INPUT_SEPARATOR = \";\";\n\nconst BizDeployNodeConfigFieldsProviderBaotaPanelGo = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"siteType\"]}\n        initialValue={initialValues.siteType}\n        label={t(\"workflow_node.deploy.form.baotapanelgo_site_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[\"php\", \"java\", \"asp\", \"go\", \"python\", \"nodejs\", \"proxy\", \"general\"].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.baotapanelgo_site_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"siteNames\"]}\n        initialValue={initialValues.siteNames}\n        label={t(\"workflow_node.deploy.form.baotapanelgo_site_names.label\")}\n        extra={t(\"workflow_node.deploy.form.baotapanelgo_site_names.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.baotapanelgo_site_names.tooltip\") }}></span>}\n      >\n        <MultipleSplitValueInput\n          modalTitle={t(\"workflow_node.deploy.form.baotapanelgo_site_names.multiple_input_modal.title\")}\n          placeholder={t(\"workflow_node.deploy.form.baotapanelgo_site_names.placeholder\")}\n          placeholderInModal={t(\"workflow_node.deploy.form.baotapanelgo_site_names.multiple_input_modal.placeholder\")}\n          separator={MULTIPLE_INPUT_SEPARATOR}\n          splitOptions={{ removeEmpty: true, trimSpace: true }}\n        />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    siteType: \"php\",\n    siteNames: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    siteType: z.string().nonempty(t(\"workflow_node.deploy.form.baotapanelgo_site_type.placeholder\")),\n    siteNames: z\n      .string()\n      .nonempty(t(\"workflow_node.deploy.form.baotapanelgo_site_names.placeholder\"))\n      .refine(\n        (v) => {\n          if (!v) return false;\n          return v.split(MULTIPLE_INPUT_SEPARATOR).every((s) => !!s.trim());\n        },\n        { error: t(\"workflow_node.deploy.form.baotapanelgo_site_names.placeholder\") }\n      ),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderBaotaPanelGo, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaotaPanelGoConsole.tsx",
    "content": "import { getI18n } from \"react-i18next\";\nimport { z } from \"zod\";\n\nconst BizDeployNodeConfigFieldsProviderBaotaPanelConsoleGo = () => {\n  return <></>;\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {};\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t: _ } = i18n;\n\n  return z.object({});\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderBaotaPanelConsoleGo, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaotaWAF.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, InputNumber } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport MultipleSplitValueInput from \"@/components/MultipleSplitValueInput\";\nimport Tips from \"@/components/Tips\";\nimport { isPortNumber } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst MULTIPLE_INPUT_SEPARATOR = \";\";\n\nconst BizDeployNodeConfigFieldsProviderBaotaWAF = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.baotawaf.guide\") }}></span>} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"siteNames\"]}\n        initialValue={initialValues.siteNames}\n        label={t(\"workflow_node.deploy.form.baotawaf_site_names.label\")}\n        extra={t(\"workflow_node.deploy.form.baotawaf_site_names.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.baotawaf_site_names.tooltip\") }}></span>}\n      >\n        <MultipleSplitValueInput\n          modalTitle={t(\"workflow_node.deploy.form.baotawaf_site_names.multiple_input_modal.title\")}\n          placeholder={t(\"workflow_node.deploy.form.baotawaf_site_names.placeholder\")}\n          placeholderInModal={t(\"workflow_node.deploy.form.baotawaf_site_names.multiple_input_modal.placeholder\")}\n          separator={MULTIPLE_INPUT_SEPARATOR}\n          splitOptions={{ removeEmpty: true, trimSpace: true }}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"sitePort\"]}\n        initialValue={initialValues.sitePort}\n        label={t(\"workflow_node.deploy.form.baotawaf_site_port.label\")}\n        rules={[formRule]}\n      >\n        <InputNumber style={{ width: \"100%\" }} placeholder={t(\"access.form.ssh_port.placeholder\")} min={1} max={65535} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    siteNames: \"\",\n    sitePort: 443,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    siteNames: z\n      .string()\n      .nonempty(t(\"workflow_node.deploy.form.baotawaf_site_names.placeholder\"))\n      .refine(\n        (v) => {\n          if (!v) return false;\n          return v.split(MULTIPLE_INPUT_SEPARATOR).every((s) => !!s.trim());\n        },\n        { error: t(\"workflow_node.deploy.form.baotawaf_site_names.placeholder\") }\n      ),\n    sitePort: z.coerce.number().refine((v) => isPortNumber(v), t(\"common.errmsg.port_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderBaotaWAF, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaotaWAFConsole.tsx",
    "content": "import type { getI18n } from \"react-i18next\";\nimport { useTranslation } from \"react-i18next\";\nimport { Form } from \"antd\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\n\nconst BizDeployNodeConfigFieldsProviderBaotaWAFConsole = () => {\n  const { t } = useTranslation();\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.baotawaf_console.guide\") }}></span>} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {};\n};\n\nconst getSchema = (_: { i18n?: ReturnType<typeof getI18n> }) => {\n  return z.object({});\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderBaotaWAFConsole, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBunnyCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderBunnyCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"pullZoneId\"]}\n        initialValue={initialValues.pullZoneId}\n        label={t(\"workflow_node.deploy.form.bunny_cdn_pull_zone_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.bunny_cdn_pull_zone_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.bunny_cdn_pull_zone_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"hostname\"]}\n        initialValue={initialValues.hostname}\n        label={t(\"workflow_node.deploy.form.bunny_cdn_hostname.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.bunny_cdn_hostname.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.bunny_cdn_hostname.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    pullZoneId: \"\",\n    hostname: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    pullZoneId: z.union([z.string(), z.number().int()]).refine((v) => {\n      return /^\\d+$/.test(v + \"\") && +v! > 0;\n    }, t(\"workflow_node.deploy.form.bunny_cdn_pull_zone_id.placeholder\")),\n    hostname: z\n      .string()\n      .nonempty(t(\"workflow_node.deploy.form.bunny_cdn_hostname.placeholder\"))\n      .refine((v) => {\n        return isDomain(v!, { allowWildcard: true });\n      }, t(\"common.errmsg.domain_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderBunnyCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBytePlusCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderBytePlusCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.byteplus_cdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.byteplus_cdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderBytePlusCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderCPanel.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_WEBSITE = \"website\" as const;\n\nconst BizDeployNodeConfigFieldsProviderCPanel = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_WEBSITE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.cpanel_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_WEBSITE}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.cpanel_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.cpanel_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceType: RESOURCE_TYPE_WEBSITE,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      resourceType: z.literal(RESOURCE_TYPE_WEBSITE, t(\"workflow_node.deploy.form.cpanel_resource_type.placeholder\")),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_WEBSITE:\n          {\n            if (!isDomain(values.domain!)) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.cpanel_domain.placeholder\"),\n                path: [\"domain\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderCPanel, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderCTCCCloudAO.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderCTCCCloudAO = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.ctcccloud_ao_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.ctcccloud_ao_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderCTCCCloudAO, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderCTCCCloudCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderCTCCCloudCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.ctcccloud_cdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.ctcccloud_cdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderCTCCCloudCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderCTCCCloudELB.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_LOADBALANCER = \"loadbalancer\" as const;\nconst RESOURCE_TYPE_LISTENER = \"listener\" as const;\n\nconst BizDeployNodeConfigFieldsProviderCTCCCloudELB = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"regionId\"]}\n        initialValue={initialValues.regionId}\n        label={t(\"workflow_node.deploy.form.ctcccloud_elb_region_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ctcccloud_elb_region_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.ctcccloud_elb_region_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.ctcccloud_elb_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER}>\n        <Form.Item\n          name={[parentNamePath, \"loadbalancerId\"]}\n          initialValue={initialValues.loadbalancerId}\n          label={t(\"workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"listenerId\"]}\n          initialValue={initialValues.listenerId}\n          label={t(\"workflow_node.deploy.form.ctcccloud_elb_listener_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ctcccloud_elb_listener_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.ctcccloud_elb_listener_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    regionId: \"\",\n    resourceType: RESOURCE_TYPE_LISTENER,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      regionId: z.string().nonempty(t(\"workflow_node.deploy.form.ctcccloud_elb_region_id.placeholder\")),\n      resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      loadbalancerId: z.string().nullish(),\n      listenerId: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_LOADBALANCER:\n          {\n            if (!values.loadbalancerId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.placeholder\"),\n                path: [\"loadbalancerId\"],\n              });\n            }\n          }\n          break;\n\n        case RESOURCE_TYPE_LISTENER:\n          {\n            if (!values.listenerId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.ctcccloud_elb_listener_id.placeholder\"),\n                path: [\"listenerId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderCTCCCloudELB, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderCTCCCloudFaaS.tsx",
    "content": "﻿import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderCTCCCloudFaaS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"regionId\"]}\n        initialValue={initialValues.regionId}\n        label={t(\"workflow_node.deploy.form.ctcccloud_faas_region_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ctcccloud_faas_region_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.ctcccloud_faas_region_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.ctcccloud_faas_domain.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.ctcccloud_faas_domain.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    regionId: \"\",\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    regionId: z.string().nonempty(t(\"workflow_node.deploy.form.ctcccloud_faas_region_id.placeholder\")),\n    domain: z.string().refine((v) => isDomain(v), t(\"common.errmsg.domain_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderCTCCCloudFaaS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderCTCCCloudICDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderCTCCCloudICDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.ctcccloud_icdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.ctcccloud_icdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderCTCCCloudICDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderCTCCCloudLVDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderCTCCCloudLVDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.ctcccloud_lvdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.ctcccloud_lvdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n            {\n              if (!isDomain(values.domain!)) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderCTCCCloudLVDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderCdnfly.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_WEBSITE = \"website\" as const;\nconst RESOURCE_TYPE_CERTIFICATE = \"certificate\" as const;\n\nconst BizDeployNodeConfigFieldsProviderCdnfly = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_WEBSITE, RESOURCE_TYPE_CERTIFICATE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.cdnfly_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_WEBSITE}>\n        <Form.Item\n          name={[parentNamePath, \"siteId\"]}\n          initialValue={initialValues.siteId}\n          label={t(\"workflow_node.deploy.form.cdnfly_site_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.cdnfly_site_id.tooltip\") }}></span>}\n        >\n          <Input type=\"number\" placeholder={t(\"workflow_node.deploy.form.cdnfly_site_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>\n        <Form.Item\n          name={[parentNamePath, \"certificateId\"]}\n          initialValue={initialValues.certificateId}\n          label={t(\"workflow_node.deploy.form.cdnfly_certificate_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.cdnfly_certificate_id.tooltip\") }}></span>}\n        >\n          <Input type=\"number\" placeholder={t(\"workflow_node.deploy.form.cdnfly_certificate_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceType: RESOURCE_TYPE_WEBSITE,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      resourceType: z.literal([RESOURCE_TYPE_WEBSITE, RESOURCE_TYPE_CERTIFICATE], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      siteId: z.union([z.string(), z.number().int()]).nullish(),\n      certificateId: z.union([z.string(), z.number().int()]).nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_WEBSITE:\n          {\n            const scSiteId = z.coerce.number().int().positive();\n            if (!scSiteId.safeParse(values.siteId).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.cdnfly_site_id.placeholder\"),\n                path: [\"siteId\"],\n              });\n            }\n          }\n          break;\n\n        case RESOURCE_TYPE_CERTIFICATE:\n          {\n            const scCertificateId = z.coerce.number().int().positive();\n            if (!scCertificateId.safeParse(values.certificateId).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.cdnfly_certificate_id.placeholder\"),\n                path: [\"certificateId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderCdnfly, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderDogeCloudCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderDogeCloudCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.dogecloud_cdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.dogecloud_cdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n            {\n              if (!isDomain(values.domain!)) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderDogeCloudCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderFlexCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_CERTIFICATE = \"certificate\" as const;\n\nconst BizDeployNodeConfigFieldsProviderFlexCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_CERTIFICATE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.flexcdn_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>\n        <Form.Item\n          name={[parentNamePath, \"certificateId\"]}\n          initialValue={initialValues.certificateId}\n          label={t(\"workflow_node.deploy.form.flexcdn_certificate_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.flexcdn_certificate_id.tooltip\") }}></span>}\n        >\n          <Input type=\"number\" placeholder={t(\"workflow_node.deploy.form.flexcdn_certificate_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceType: RESOURCE_TYPE_CERTIFICATE,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      certificateId: z.union([z.string(), z.number().int()]).nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_CERTIFICATE:\n          {\n            const scCertificateId = z.coerce.number().int().positive();\n            if (!scCertificateId.safeParse(values.certificateId).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.flexcdn_certificate_id.placeholder\"),\n                path: [\"certificateId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderFlexCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderFlyIO.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderFlyIO = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"appName\"]}\n        initialValue={initialValues.appName}\n        label={t(\"workflow_node.deploy.form.flyio_app_name.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.flyio_app_name.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.flyio_domain.label\")}\n        extra={t(\"workflow_node.deploy.form.flyio_domain.help\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.flyio_domain.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    appName: \"\",\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    appName: z.string().nonempty(t(\"workflow_node.deploy.form.flyio_app_name.placeholder\")),\n    domain: z.string().refine((v) => isDomain(v, { allowWildcard: true }), t(\"common.errmsg.domain_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderFlyIO, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderGcoreCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderGcoreCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"resourceId\"]}\n        initialValue={initialValues.resourceId}\n        label={t(\"workflow_node.deploy.form.gcore_cdn_resource_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.gcore_cdn_resource_id.tooltip\") }}></span>}\n      >\n        <Input type=\"number\" placeholder={t(\"workflow_node.deploy.form.gcore_cdn_resource_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"certificateId\"]}\n        initialValue={initialValues.certificateId}\n        label={t(\"workflow_node.deploy.form.gcore_cdn_certificate_id.label\")}\n        extra={t(\"workflow_node.deploy.form.gcore_cdn_certificate_id.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.gcore_cdn_certificate_id.tooltip\") }}></span>}\n      >\n        <Input allowClear type=\"number\" placeholder={t(\"workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceId: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    resourceId: z.union([z.string(), z.number().int()]).refine((v) => {\n      return /^\\d+$/.test(v + \"\") && +v > 0;\n    }, t(\"workflow_node.deploy.form.gcore_cdn_resource_id.placeholder\")),\n    certificateId: z\n      .union([z.string(), z.number().int()])\n      .nullish()\n      .refine((v) => {\n        if (!v) return true;\n        return /^\\d+$/.test(v + \"\") && +v > 0;\n      }, t(\"workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderGcoreCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderGoEdge.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_CERTIFICATE = \"certificate\" as const;\n\nconst BizDeployNodeConfigFieldsProviderGoEdge = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_CERTIFICATE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.goedge_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>\n        <Form.Item\n          name={[parentNamePath, \"certificateId\"]}\n          initialValue={initialValues.certificateId}\n          label={t(\"workflow_node.deploy.form.goedge_certificate_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.goedge_certificate_id.tooltip\") }}></span>}\n        >\n          <Input type=\"number\" placeholder={t(\"workflow_node.deploy.form.goedge_certificate_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceType: RESOURCE_TYPE_CERTIFICATE,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t(\"workflow_node.deploy.form.goedge_resource_type.placeholder\")),\n      certificateId: z.union([z.string(), z.number().int()]).nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_CERTIFICATE:\n          {\n            const scCertificateId = z.coerce.number().int().positive();\n            if (!scCertificateId.safeParse(values.certificateId).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.goedge_certificate_id.placeholder\"),\n                path: [\"certificateId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderGoEdge, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderHuaweiCloudCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderHuaweiCloudCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.huaweicloud_cdn_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.huaweicloud_cdn_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.huaweicloud_cdn_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.huaweicloud_cdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.huaweicloud_cdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.huaweicloud_cdn_region.placeholder\")),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderHuaweiCloudCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderHuaweiCloudELB.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_LOADBALANCER = \"loadbalancer\" as const;\nconst RESOURCE_TYPE_LISTENER = \"listener\" as const;\nconst RESOURCE_TYPE_CERTIFICATE = \"certificate\" as const;\n\nconst BizDeployNodeConfigFieldsProviderHuaweiCloudELB = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.huaweicloud_elb_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.huaweicloud_elb_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.huaweicloud_elb_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER, RESOURCE_TYPE_CERTIFICATE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.huaweicloud_elb_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>\n        <Form.Item\n          name={[parentNamePath, \"certificateId\"]}\n          initialValue={initialValues.certificateId}\n          label={t(\"workflow_node.deploy.form.huaweicloud_elb_certificate_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.huaweicloud_elb_certificate_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.huaweicloud_elb_certificate_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER}>\n        <Form.Item\n          name={[parentNamePath, \"loadbalancerId\"]}\n          initialValue={initialValues.loadbalancerId}\n          label={t(\"workflow_node.deploy.form.huaweicloud_elb_loadbalancer_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.huaweicloud_elb_loadbalancer_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.huaweicloud_elb_loadbalancer_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"listenerId\"]}\n          initialValue={initialValues.listenerId}\n          label={t(\"workflow_node.deploy.form.huaweicloud_elb_listener_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.huaweicloud_elb_listener_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.huaweicloud_elb_listener_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    resourceType: RESOURCE_TYPE_LISTENER,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.huaweicloud_elb_region.placeholder\")),\n      resourceType: z.literal(\n        [RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER, RESOURCE_TYPE_CERTIFICATE],\n        t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")\n      ),\n      loadbalancerId: z.string().nullish(),\n      listenerId: z.string().nullish(),\n      certificateId: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_LOADBALANCER:\n          {\n            if (!values.loadbalancerId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.huaweicloud_elb_loadbalancer_id.placeholder\"),\n                path: [\"loadbalancerId\"],\n              });\n            }\n          }\n          break;\n\n        case RESOURCE_TYPE_LISTENER:\n          {\n            if (!values.listenerId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.huaweicloud_elb_listener_id.placeholder\"),\n                path: [\"listenerId\"],\n              });\n            }\n          }\n          break;\n\n        case RESOURCE_TYPE_CERTIFICATE:\n          {\n            if (!values.certificateId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.huaweicloud_elb_certificate_id.placeholder\"),\n                path: [\"certificateId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderHuaweiCloudELB, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderHuaweiCloudOBS.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderHuaweiCloudOBS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.huaweicloud_obs_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.huaweicloud_obs_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.huaweicloud_obs_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"bucket\"]}\n        initialValue={initialValues.bucket}\n        label={t(\"workflow_node.deploy.form.huaweicloud_obs_bucket.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.huaweicloud_obs_bucket.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.huaweicloud_obs_domain.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.huaweicloud_obs_domain.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    bucket: \"\",\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.deploy.form.huaweicloud_obs_region.placeholder\")),\n    bucket: z.string().nonempty(t(\"workflow_node.deploy.form.huaweicloud_obs_bucket.placeholder\")),\n    domain: z.string().refine((v) => isDomain(v, { allowWildcard: true }), t(\"common.errmsg.domain_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderHuaweiCloudOBS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderHuaweiCloudWAF.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_CLOUDSERVER = \"cloudserver\" as const;\nconst RESOURCE_TYPE_PREMIUMHOST = \"premiumhost\" as const;\nconst RESOURCE_TYPE_CERTIFICATE = \"certificate\" as const;\n\nconst BizDeployNodeConfigFieldsProviderHuaweiCloudWAF = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.huaweicloud_waf_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.huaweicloud_waf_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.huaweicloud_waf_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_CLOUDSERVER, RESOURCE_TYPE_PREMIUMHOST, RESOURCE_TYPE_CERTIFICATE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.huaweicloud_waf_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_CLOUDSERVER || fieldResourceType === RESOURCE_TYPE_PREMIUMHOST}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.huaweicloud_waf_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.huaweicloud_waf_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>\n        <Form.Item\n          name={[parentNamePath, \"certificateId\"]}\n          initialValue={initialValues.certificateId}\n          label={t(\"workflow_node.deploy.form.huaweicloud_waf_certificate_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.huaweicloud_waf_certificate_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.huaweicloud_waf_certificate_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.huaweicloud_waf_region.placeholder\")),\n      resourceType: z.literal(\n        [RESOURCE_TYPE_CLOUDSERVER, RESOURCE_TYPE_PREMIUMHOST, RESOURCE_TYPE_CERTIFICATE],\n        t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")\n      ),\n      certificateId: z.string().nullish(),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_CLOUDSERVER:\n        case RESOURCE_TYPE_PREMIUMHOST:\n          {\n            if (!isDomain(values.domain!, { allowWildcard: true })) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.huaweicloud_waf_domain.placeholder\"),\n                path: [\"domain\"],\n              });\n            }\n          }\n          break;\n\n        case RESOURCE_TYPE_CERTIFICATE:\n          {\n            if (!values.certificateId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.huaweicloud_waf_certificate_id.placeholder\"),\n                path: [\"certificateId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderHuaweiCloudWAF, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderJDCloudALB.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_LOADBALANCER = \"loadbalancer\" as const;\nconst RESOURCE_TYPE_LISTENER = \"listener\" as const;\n\nconst BizDeployNodeConfigFieldsProviderJDCloudALB = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"regionId\"]}\n        initialValue={initialValues.regionId}\n        label={t(\"workflow_node.deploy.form.jdcloud_alb_region_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.jdcloud_alb_region_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.jdcloud_alb_region_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.jdcloud_alb_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER}>\n        <Form.Item\n          name={[parentNamePath, \"loadbalancerId\"]}\n          initialValue={initialValues.loadbalancerId}\n          label={t(\"workflow_node.deploy.form.jdcloud_alb_loadbalancer_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.jdcloud_alb_loadbalancer_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.jdcloud_alb_loadbalancer_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"listenerId\"]}\n          initialValue={initialValues.listenerId}\n          label={t(\"workflow_node.deploy.form.jdcloud_alb_listener_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.jdcloud_alb_listener_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.jdcloud_alb_listener_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER || fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.jdcloud_alb_snidomain.label\")}\n          extra={t(\"workflow_node.deploy.form.jdcloud_alb_snidomain.help\")}\n          rules={[formRule]}\n        >\n          <Input allowClear placeholder={t(\"workflow_node.deploy.form.jdcloud_alb_snidomain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    regionId: \"\",\n    resourceType: RESOURCE_TYPE_LISTENER,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      regionId: z.string().nonempty(t(\"workflow_node.deploy.form.jdcloud_alb_region_id.placeholder\")),\n      resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      loadbalancerId: z.string().nullish(),\n      listenerId: z.string().nullish(),\n      domain: z\n        .string()\n        .nullish()\n        .refine((v) => {\n          if (!v) return true;\n          return isDomain(v, { allowWildcard: true });\n        }, t(\"common.errmsg.domain_invalid\")),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_LOADBALANCER:\n          {\n            if (!values.loadbalancerId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.jdcloud_alb_loadbalancer_id.placeholder\"),\n                path: [\"loadbalancerId\"],\n              });\n            }\n          }\n          break;\n\n        case RESOURCE_TYPE_LISTENER:\n          {\n            if (!values.listenerId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.jdcloud_alb_listener_id.placeholder\"),\n                path: [\"listenerId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderJDCloudALB, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderJDCloudCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderJDCloudCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.jdcloud_cdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.jdcloud_cdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderJDCloudCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderJDCloudLive.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderJDCloudLive = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.jdcloud_live_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.jdcloud_live_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n            {\n              if (!isDomain(values.domain!)) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderJDCloudLive, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderJDCloudVOD.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderJDCloudVOD = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.jdcloud_vod_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.jdcloud_vod_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n            {\n              if (!isDomain(values.domain!)) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderJDCloudVOD, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderKong.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_CERTIFICATE = \"certificate\" as const;\n\nconst BizDeployNodeConfigFieldsProviderKong = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.kong.guide\") }}></span>} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_CERTIFICATE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.kong_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"workspace\"]}\n        initialValue={initialValues.workspace}\n        label={t(\"workflow_node.deploy.form.kong_workspace.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.kong_workspace.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.kong_workspace.placeholder\")} />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>\n        <Form.Item\n          name={[parentNamePath, \"certificateId\"]}\n          initialValue={initialValues.certificateId}\n          label={t(\"workflow_node.deploy.form.kong_certificate_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.kong_certificate_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.kong_certificate_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceType: RESOURCE_TYPE_CERTIFICATE,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      workspace: z.string().nullish(),\n      certificateId: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_CERTIFICATE:\n          {\n            if (!values.certificateId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.kong_certificate_id.placeholder\"),\n                path: [\"certificateId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderKong, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderKsyunCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_DOMAIN = \"domain\" as const;\nconst RESOURCE_TYPE_CERTIFICATE = \"certificate\" as const;\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderKsyunCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_DOMAIN, RESOURCE_TYPE_CERTIFICATE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.ksyun_cdn_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_DOMAIN}>\n        <Form.Item\n          name={[parentNamePath, \"domainMatchPattern\"]}\n          initialValue={initialValues.domainMatchPattern}\n          label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n          extra={\n            fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n              <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n            ) : (\n              void 0\n            )\n          }\n          rules={[formRule]}\n        >\n          <Radio.Group\n            options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n              key: s,\n              label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n              value: s,\n            }))}\n          />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.ksyun_cdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.ksyun_cdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>\n        <Form.Item\n          name={[parentNamePath, \"certificateId\"]}\n          initialValue={initialValues.certificateId}\n          label={t(\"workflow_node.deploy.form.ksyun_cdn_certificate_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ksyun_cdn_certificate_id.tooltip\") }}></span>}\n        >\n          <Input allowClear placeholder={t(\"workflow_node.deploy.form.ksyun_cdn_certificate_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceType: RESOURCE_TYPE_DOMAIN,\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      resourceType: z.literal([RESOURCE_TYPE_DOMAIN, RESOURCE_TYPE_CERTIFICATE], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n      certificateId: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_DOMAIN:\n          {\n            if (!values.domainMatchPattern) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\"),\n                path: [\"domainMatchPattern\"],\n              });\n            }\n\n            switch (values.domainMatchPattern) {\n              case DOMAIN_MATCH_PATTERN_EXACT:\n              case DOMAIN_MATCH_PATTERN_WILDCARD:\n                {\n                  if (!isDomain(values.domain!, { allowWildcard: true })) {\n                    ctx.addIssue({\n                      code: \"custom\",\n                      message: t(\"common.errmsg.domain_invalid\"),\n                      path: [\"domain\"],\n                    });\n                  }\n                }\n                break;\n            }\n          }\n          break;\n\n        case RESOURCE_TYPE_CERTIFICATE:\n          {\n            if (!values.certificateId) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.ksyun_cdn_certificate_id.placeholder\"),\n                path: [\"certificateId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderKsyunCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderKubernetesSecret.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport CodeTextInput from \"@/components/CodeTextInput\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderKubernetesSecret = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const handleSecretAnnotationsBlur = () => {\n    let value = formInst.getFieldValue([parentNamePath, \"secretAnnotations\"]);\n    value = value.trim();\n    value = value.replace(/(?<!\\r)\\n/g, \"\\r\\n\");\n    formInst.setFieldValue([parentNamePath, \"secretAnnotations\"], value);\n  };\n\n  const handleSecretLabelsBlur = () => {\n    let value = formInst.getFieldValue([parentNamePath, \"secretLabels\"]);\n    value = value.trim();\n    value = value.replace(/(?<!\\r)\\n/g, \"\\r\\n\");\n    formInst.setFieldValue([parentNamePath, \"secretLabels\"], value);\n  };\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"namespace\"]}\n        initialValue={initialValues.namespace}\n        label={t(\"workflow_node.deploy.form.k8s_namespace.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.k8s_namespace.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.k8s_namespace.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretName\"]}\n        initialValue={initialValues.secretName}\n        label={t(\"workflow_node.deploy.form.k8s_secret_name.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.k8s_secret_name.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.k8s_secret_name.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretType\"]}\n        initialValue={initialValues.secretType}\n        label={t(\"workflow_node.deploy.form.k8s_secret_type.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.k8s_secret_type.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.k8s_secret_type.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretDataKeyForCrt\"]}\n        initialValue={initialValues.secretDataKeyForCrt}\n        label={t(\"workflow_node.deploy.form.k8s_secret_data_key_for_crt.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.k8s_secret_data_key_for_crt.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.k8s_secret_data_key_for_crt.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretDataKeyForKey\"]}\n        initialValue={initialValues.secretDataKeyForKey}\n        label={t(\"workflow_node.deploy.form.k8s_secret_data_key_for_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.k8s_secret_data_key_for_key.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretAnnotations\"]}\n        initialValue={initialValues.secretAnnotations}\n        label={t(\"workflow_node.deploy.form.k8s_secret_annotations.label\")}\n        extra={t(\"workflow_node.deploy.form.k8s_secret_annotations.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.k8s_secret_annotations.tooltip\") }}></span>}\n      >\n        <CodeTextInput\n          lineWrapping={false}\n          height=\"auto\"\n          minHeight=\"64px\"\n          maxHeight=\"256px\"\n          placeholder={t(\"workflow_node.deploy.form.k8s_secret_annotations.placeholder\")}\n          onBlur={handleSecretAnnotationsBlur}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"secretLabels\"]}\n        initialValue={initialValues.secretLabels}\n        label={t(\"workflow_node.deploy.form.k8s_secret_labels.label\")}\n        extra={t(\"workflow_node.deploy.form.k8s_secret_labels.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.k8s_secret_labels.tooltip\") }}></span>}\n      >\n        <CodeTextInput\n          lineWrapping={false}\n          height=\"auto\"\n          minHeight=\"64px\"\n          maxHeight=\"256px\"\n          placeholder={t(\"workflow_node.deploy.form.k8s_secret_labels.placeholder\")}\n          onBlur={handleSecretLabelsBlur}\n        />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    namespace: \"default\",\n    secretType: \"kubernetes.io/tls\",\n    secretDataKeyForCrt: \"tls.crt\",\n    secretDataKeyForKey: \"tls.key\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    namespace: z.string().nonempty(t(\"workflow_node.deploy.form.k8s_namespace.placeholder\")),\n    secretName: z.string().nonempty(t(\"workflow_node.deploy.form.k8s_secret_name.placeholder\")),\n    secretType: z.string().nonempty(t(\"workflow_node.deploy.form.k8s_secret_type.placeholder\")),\n    secretDataKeyForCrt: z.string().nonempty(t(\"workflow_node.deploy.form.k8s_secret_data_key_for_crt.placeholder\")),\n    secretDataKeyForKey: z.string().nonempty(t(\"workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder\")),\n    secretAnnotations: z\n      .string()\n      .nullish()\n      .refine((v) => {\n        if (!v) return true;\n\n        const lines = v.split(/\\r?\\n/);\n        for (const line of lines) {\n          if (line.split(\":\").length < 2) {\n            return false;\n          }\n        }\n        return true;\n      }, t(\"workflow_node.deploy.form.k8s_secret_annotations.errmsg.invalid\")),\n    secretLabels: z\n      .string()\n      .nullish()\n      .refine((v) => {\n        if (!v) return true;\n\n        const lines = v.split(/\\r?\\n/);\n        for (const line of lines) {\n          if (line.split(\":\").length < 2) {\n            return false;\n          }\n        }\n        return true;\n      }, t(\"workflow_node.deploy.form.k8s_secret_labels.errmsg.invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderKubernetesSecret, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderLeCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_CERTIFICATE = \"certificate\" as const;\n\nconst BizDeployNodeConfigFieldsProviderLeCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_CERTIFICATE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.lecdn_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>\n        <Form.Item\n          name={[parentNamePath, \"certificateId\"]}\n          initialValue={initialValues.certificateId}\n          label={t(\"workflow_node.deploy.form.lecdn_certificate_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.lecdn_certificate_id.tooltip\") }}></span>}\n        >\n          <Input type=\"number\" placeholder={t(\"workflow_node.deploy.form.lecdn_certificate_id.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"clientId\"]}\n          initialValue={initialValues.clientId}\n          label={t(\"workflow_node.deploy.form.lecdn_client_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.lecdn_client_id.tooltip\") }}></span>}\n        >\n          <Input type=\"number\" allowClear placeholder={t(\"workflow_node.deploy.form.lecdn_client_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceType: RESOURCE_TYPE_CERTIFICATE,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      certificateId: z.union([z.string(), z.number().int()]).nullish(),\n      clientId: z.union([z.string(), z.number().int()]).nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_CERTIFICATE:\n          {\n            const scCertificateId = z.coerce.number().int().positive();\n            if (!scCertificateId.safeParse(values.certificateId).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.lecdn_certificate_id.placeholder\"),\n                path: [\"certificateId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderLeCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderLocal.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { IconBulb, IconChevronDown } from \"@tabler/icons-react\";\nimport { Button, Divider, Form, Input, Popover, Select, Space } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport CodeTextInput from \"@/components/CodeTextInput\";\nimport PresetScriptTemplatesPopselect from \"@/components/preset/PresetScriptTemplatesPopselect\";\nimport Show from \"@/components/Show\";\nimport Tips from \"@/components/Tips\";\nimport { CERTIFICATE_FORMATS } from \"@/domain/certificate\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst FORMAT_PEM = CERTIFICATE_FORMATS.PEM;\nconst FORMAT_PFX = CERTIFICATE_FORMATS.PFX;\nconst FORMAT_JKS = CERTIFICATE_FORMATS.JKS;\n\nconst SHELLENV_SH = \"sh\" as const;\nconst SHELLENV_CMD = \"cmd\" as const;\nconst SHELLENV_POWERSHELL = \"powershell\" as const;\n\nexport const initPresetScript = (\n  key: \"sh_backup_files\" | \"ps_backup_files\" | \"sh_reload_nginx\" | \"ps_binding_iis\" | \"ps_binding_netsh\" | \"ps_binding_rdp\",\n  params?: {\n    certPath?: string;\n    certPathForServerOnly?: string;\n    certPathForIntermediaOnly?: string;\n    keyPath?: string;\n    pfxPassword?: string;\n    jksAlias?: string;\n    jksKeypass?: string;\n    jksStorepass?: string;\n  }\n) => {\n  switch (key) {\n    case \"sh_backup_files\":\n      return `# 请将以下路径替换为实际值\ncp \"${params?.certPath || \"<your-cert-path>\"}\" \"${params?.certPath || \"<your-cert-path>\"}.bak\" 2>/dev/null || :\ncp \"${params?.keyPath || \"<your-key-path>\"}\" \"${params?.keyPath || \"<your-key-path>\"}.bak\" 2>/dev/null || :\n      `.trim();\n\n    case \"ps_backup_files\":\n      return `# 请将以下路径替换为实际值\nif (Test-Path -Path \"${params?.certPath || \"<your-cert-path>\"}\" -PathType Leaf) {\n  Copy-Item -Path \"${params?.certPath || \"<your-cert-path>\"}\" -Destination \"${params?.certPath || \"<your-cert-path>\"}.bak\" -Force\n}\nif (Test-Path -Path \"${params?.keyPath || \"<your-key-path>\"}\" -PathType Leaf) {\n  Copy-Item -Path \"${params?.keyPath || \"<your-key-path>\"}\" -Destination \"${params?.keyPath || \"<your-key-path>\"}.bak\" -Force\n}\n      `.trim();\n\n    case \"sh_reload_nginx\":\n      return `# *** 需要 root 权限 ***\n\nsudo service nginx reload\n      `.trim();\n\n    case \"ps_binding_iis\":\n      return `# *** 需要管理员权限 ***\n\n# 请将以下变量替换为实际值\n$pfxPath = \"\\${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}\" # PFX 文件路径（与表单中保持一致）\n$pfxPassword = \"\\${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}\" # PFX 密码（与表单中保持一致）\n$siteName = \"<your-site-name>\" # IIS 网站名称\n$domain = \"<your-domain-name>\" # 域名\n$ipaddr = \"<your-binding-ip>\"  # 绑定 IP，“*”表示所有 IP 绑定\n$port = \"<your-binding-port>\"  # 绑定端口\n\n# 导入证书到本地计算机的个人存储区\n$cert = Import-PfxCertificate -FilePath \"$pfxPath\" -CertStoreLocation Cert:\\\\LocalMachine\\\\My -Password (ConvertTo-SecureString -String \"$pfxPassword\" -AsPlainText -Force) -Exportable\n# 获取 Thumbprint\n$thumbprint = $cert.Thumbprint\n# 导入 WebAdministration 模块\nImport-Module WebAdministration\n# 检查是否已存在 HTTPS 绑定\n$existingBinding = Get-WebBinding -Name \"$siteName\" -Protocol \"https\" -Port $port -HostHeader \"$domain\" -ErrorAction SilentlyContinue\nif (!$existingBinding) {\n  # 添加新的 HTTPS 绑定\n  New-WebBinding -Name \"$siteName\" -Protocol \"https\" -Port $port -IPAddress \"$ipaddr\" -HostHeader \"$domain\"\n}\n# 获取绑定对象\n$binding = Get-WebBinding -Name \"$siteName\" -Protocol \"https\" -Port $port -IPAddress \"$ipaddr\" -HostHeader \"$domain\"\n# 绑定 SSL 证书\n$binding.AddSslCertificate($thumbprint, \"My\")\n# 删除目录下的证书文件\nRemove-Item -Path \"$pfxPath\" -Force\n      `.trim();\n\n    case \"ps_binding_netsh\":\n      return `# *** 需要管理员权限 ***\n\n# 请将以下变量替换为实际值\n$pfxPath = \"\\${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}\" # PFX 文件路径（与表单中保持一致）\n$pfxPassword = \"\\${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}\" # PFX 密码（与表单中保持一致）\n$ipaddr = \"<your-binding-ip>\"  # 绑定 IP，“0.0.0.0”表示所有 IP 绑定，可填入域名\n$port = \"<your-binding-port>\"  # 绑定端口\n\n# 导入证书到本地计算机的个人存储区\n$addr = $ipaddr + \":\" + $port\n$cert = Import-PfxCertificate -FilePath \"$pfxPath\" -CertStoreLocation Cert:\\\\LocalMachine\\\\My -Password (ConvertTo-SecureString -String \"$pfxPassword\" -AsPlainText -Force) -Exportable\n# 获取 Thumbprint\n$thumbprint = $cert.Thumbprint\n# 检测端口是否绑定证书，如绑定则删除绑定\n$isExist = netsh http show sslcert ipport=$addr\nif ($isExist -like \"*$addr*\"){ netsh http delete sslcert ipport=$addr }\n# 绑定到端口\nnetsh http add sslcert ipport=$addr certhash=$thumbprint\n# 删除目录下的证书文件\nRemove-Item -Path \"$pfxPath\" -Force\n      `.trim();\n\n    case \"ps_binding_rdp\":\n      return `# *** 需要管理员权限 ***\n\n# 请将以下变量替换为实际值\n$pfxPath = \"\\${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}\" # PFX 文件路径（与表单中保持一致）\n$pfxPassword = \"\\${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}\" # PFX 密码（与表单中保持一致）\n\n# 导入证书到本地计算机的个人存储区\n$cert = Import-PfxCertificate -FilePath \"$pfxPath\" -CertStoreLocation Cert:\\\\LocalMachine\\\\My -Password (ConvertTo-SecureString -String \"$pfxPassword\" -AsPlainText -Force) -Exportable\n# 获取 Thumbprint\n$thumbprint = $cert.Thumbprint\n# 绑定到 RDP\n$rdpCertPath = \"HKLM:\\\\SYSTEM\\\\CurrentControlSet\\\\Control\\\\Terminal Server\\\\WinStations\\\\RDP-Tcp\"\nSet-ItemProperty -Path $rdpCertPath -Name \"SSLCertificateSHA1Hash\" -Value \"$thumbprint\"\n      `.trim();\n  }\n};\n\nconst BizDeployNodeConfigFieldsProviderLocal = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldFormat = Form.useWatch([parentNamePath, \"format\"], formInst);\n  const fieldCertPath = Form.useWatch([parentNamePath, \"certPath\"], formInst);\n\n  const handleFormatSelect = (value: string) => {\n    if (fieldFormat === value) return;\n\n    switch (value) {\n      case FORMAT_PEM:\n        {\n          if (/(.pfx|.jks)$/.test(fieldCertPath)) {\n            formInst.setFieldValue([parentNamePath, \"certPath\"], fieldCertPath.replace(/(.pfx|.jks)$/, \".crt\"));\n          }\n        }\n        break;\n\n      case FORMAT_PFX:\n        {\n          if (/(.crt|.jks)$/.test(fieldCertPath)) {\n            formInst.setFieldValue([parentNamePath, \"certPath\"], fieldCertPath.replace(/(.crt|.jks)$/, \".pfx\"));\n          }\n        }\n        break;\n\n      case FORMAT_JKS:\n        {\n          if (/(.crt|.pfx)$/.test(fieldCertPath)) {\n            formInst.setFieldValue([parentNamePath, \"certPath\"], fieldCertPath.replace(/(.crt|.pfx)$/, \".jks\"));\n          }\n        }\n        break;\n    }\n  };\n\n  const handlePresetPreScriptClick = (key: string) => {\n    switch (key) {\n      case \"sh_backup_files\":\n      case \"ps_backup_files\":\n        {\n          const presetScriptParams = {\n            certPath: formInst.getFieldValue([parentNamePath, \"certPath\"]),\n            keyPath: formInst.getFieldValue([parentNamePath, \"keyPath\"]),\n          };\n          formInst.setFieldValue([parentNamePath, \"shellEnv\"], SHELLENV_SH);\n          formInst.setFieldValue([parentNamePath, \"preCommand\"], initPresetScript(key, presetScriptParams));\n        }\n        break;\n    }\n  };\n\n  const handlePresetPostScriptClick = (key: string) => {\n    switch (key) {\n      case \"sh_reload_nginx\":\n        {\n          formInst.setFieldValue([parentNamePath, \"shellEnv\"], SHELLENV_SH);\n          formInst.setFieldValue([parentNamePath, \"postCommand\"], initPresetScript(key));\n        }\n        break;\n\n      case \"ps_binding_iis\":\n      case \"ps_binding_netsh\":\n      case \"ps_binding_rdp\":\n        {\n          const presetScriptParams = {\n            certPath: formInst.getFieldValue([parentNamePath, \"certPath\"]),\n            pfxPassword: formInst.getFieldValue([parentNamePath, \"pfxPassword\"]),\n          };\n          formInst.setFieldValue([parentNamePath, \"shellEnv\"], SHELLENV_POWERSHELL);\n          formInst.setFieldValue([parentNamePath, \"postCommand\"], initPresetScript(key, presetScriptParams));\n        }\n        break;\n    }\n  };\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.local.guide\") }}></span>} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"format\"]}\n        initialValue={initialValues.format}\n        label={t(\"workflow_node.deploy.form.local_format.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[FORMAT_PEM, FORMAT_PFX, FORMAT_JKS].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.local_format.option.${s.toLowerCase()}.label`),\n            value: s,\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.local_format.placeholder\")}\n          onSelect={handleFormatSelect}\n        />\n      </Form.Item>\n\n      <Show when={fieldFormat === FORMAT_PEM}>\n        <Form.Item\n          name={[parentNamePath, \"keyPath\"]}\n          initialValue={initialValues.keyPath}\n          label={t(\"workflow_node.deploy.form.local_key_path.label\")}\n          extra={t(\"workflow_node.deploy.form.local_key_path.help\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.local_key_path.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Form.Item\n        name={[parentNamePath, \"certPath\"]}\n        initialValue={initialValues.certPath}\n        label={t(`workflow_node.deploy.form.local_${fieldFormat === FORMAT_PEM ? \"fullchaincert\" : \"cert\"}_path.label`)}\n        extra={t(\"workflow_node.deploy.form.local_cert_path.help\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(`workflow_node.deploy.form.local_${fieldFormat === FORMAT_PEM ? \"fullchaincert\" : \"cert\"}_path.placeholder`)} />\n      </Form.Item>\n\n      <Show when={fieldFormat === FORMAT_PEM}>\n        <Form.Item\n          name={[parentNamePath, \"certPathForServerOnly\"]}\n          initialValue={initialValues.certPathForServerOnly}\n          label={t(\"workflow_node.deploy.form.local_servercert_path.label\")}\n          extra={t(\"workflow_node.deploy.form.local_servercert_path.help\")}\n          rules={[formRule]}\n        >\n          <Input allowClear placeholder={t(\"workflow_node.deploy.form.local_servercert_path.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"certPathForIntermediaOnly\"]}\n          initialValue={initialValues.certPathForIntermediaOnly}\n          label={t(\"workflow_node.deploy.form.local_intermediacert_path.label\")}\n          extra={t(\"workflow_node.deploy.form.local_intermediacert_path.help\")}\n          rules={[formRule]}\n        >\n          <Input allowClear placeholder={t(\"workflow_node.deploy.form.local_intermediacert_path.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldFormat === FORMAT_PFX}>\n        <Form.Item\n          name={[parentNamePath, \"pfxPassword\"]}\n          initialValue={initialValues.pfxPassword}\n          label={t(\"workflow_node.deploy.form.local_pfx_password.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.local_pfx_password.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.local_pfx_password.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldFormat === FORMAT_JKS}>\n        <Form.Item\n          name={[parentNamePath, \"jksAlias\"]}\n          initialValue={initialValues.jksAlias}\n          label={t(\"workflow_node.deploy.form.local_jks_alias.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.local_jks_alias.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.local_jks_alias.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"jksKeypass\"]}\n          initialValue={initialValues.jksKeypass}\n          label={t(\"workflow_node.deploy.form.local_jks_keypass.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.local_jks_keypass.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.local_jks_keypass.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"jksStorepass\"]}\n          initialValue={initialValues.jksStorepass}\n          label={t(\"workflow_node.deploy.form.local_jks_storepass.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.local_jks_storepass.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.local_jks_storepass.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Form.Item\n        name={[parentNamePath, \"shellEnv\"]}\n        initialValue={initialValues.shellEnv}\n        label={t(\"workflow_node.deploy.form.local_shell_env.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[SHELLENV_SH, SHELLENV_CMD, SHELLENV_POWERSHELL].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.local_shell_env.option.${s.toLowerCase()}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Form.Item label={t(\"workflow_node.deploy.form.local_pre_command.label\")}>\n        <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n          <PresetScriptTemplatesPopselect\n            options={[\"sh_backup_files\", \"ps_backup_files\"].map((key) => ({\n              key,\n              label: t(`workflow_node.deploy.form.local_preset_scripts.${key}`),\n            }))}\n            trigger={[\"click\"]}\n            onSelect={(key, template) => {\n              if (template) {\n                formInst.setFieldValue([parentNamePath, \"preCommand\"], template.command);\n              } else {\n                handlePresetPreScriptClick(key);\n              }\n            }}\n          >\n            <Button size=\"small\" type=\"link\">\n              {t(\"preset.dropdown.script.button\")}\n              <IconChevronDown size=\"1.25em\" />\n            </Button>\n          </PresetScriptTemplatesPopselect>\n        </div>\n        <Form.Item name={[parentNamePath, \"preCommand\"]} initialValue={initialValues.preCommand} noStyle rules={[formRule]}>\n          <CodeTextInput\n            height=\"auto\"\n            minHeight=\"64px\"\n            maxHeight=\"256px\"\n            language={[\"shell\", \"powershell\"]}\n            placeholder={t(\"workflow_node.deploy.form.local_pre_command.placeholder\")}\n          />\n        </Form.Item>\n      </Form.Item>\n\n      <Form.Item label={t(\"workflow_node.deploy.form.local_post_command.label\")}>\n        <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n          <Space align=\"center\" separator={<Divider orientation=\"vertical\" />} size={0}>\n            <Popover content={<div dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_script_command.vartips\") }} />} mouseEnterDelay={1}>\n              <Button color=\"default\" size=\"small\" variant=\"link\">\n                <IconBulb size=\"1.25em\" />\n              </Button>\n            </Popover>\n            <PresetScriptTemplatesPopselect\n              options={[\"sh_reload_nginx\", \"ps_binding_iis\", \"ps_binding_netsh\", \"ps_binding_rdp\"].map((key) => ({\n                key,\n                label: t(`workflow_node.deploy.form.local_preset_scripts.${key}`),\n                onClick: () => handlePresetPostScriptClick(key),\n              }))}\n              trigger={[\"click\"]}\n              onSelect={(key, template) => {\n                if (template) {\n                  formInst.setFieldValue([parentNamePath, \"postCommand\"], template.command);\n                } else {\n                  handlePresetPostScriptClick(key);\n                }\n              }}\n            >\n              <Button size=\"small\" type=\"link\">\n                {t(\"preset.dropdown.script.button\")}\n                <IconChevronDown size=\"1.25em\" />\n              </Button>\n            </PresetScriptTemplatesPopselect>\n          </Space>\n        </div>\n        <Form.Item name={[parentNamePath, \"postCommand\"]} initialValue={initialValues.postCommand} noStyle rules={[formRule]}>\n          <CodeTextInput\n            height=\"auto\"\n            minHeight=\"64px\"\n            maxHeight=\"256px\"\n            language={[\"shell\", \"powershell\"]}\n            placeholder={t(\"workflow_node.deploy.form.local_post_command.placeholder\")}\n          />\n        </Form.Item>\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    format: FORMAT_PEM,\n    keyPath: \"/etc/ssl/certimate/cert.key\",\n    certPath: \"/etc/ssl/certimate/cert.crt\",\n    shellEnv: SHELLENV_SH,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      format: z.literal([FORMAT_PEM, FORMAT_PFX, FORMAT_JKS], t(\"workflow_node.deploy.form.local_format.placeholder\")),\n      keyPath: z\n        .string()\n        .max(256, t(\"common.errmsg.string_max\", { max: 256 }))\n        .nullish(),\n      certPath: z\n        .string()\n        .min(1, t(\"workflow_node.deploy.form.local_cert_path.placeholder\"))\n        .max(256, t(\"common.errmsg.string_max\", { max: 256 })),\n      certPathForServerOnly: z\n        .string()\n        .max(256, t(\"common.errmsg.string_max\", { max: 256 }))\n        .nullish(),\n      certPathForIntermediaOnly: z\n        .string()\n        .max(256, t(\"common.errmsg.string_max\", { max: 256 }))\n        .nullish(),\n      pfxPassword: z.string().nullish(),\n      jksAlias: z.string().nullish(),\n      jksKeypass: z.string().nullish(),\n      jksStorepass: z.string().nullish(),\n      shellEnv: z.literal([SHELLENV_SH, SHELLENV_CMD, SHELLENV_POWERSHELL], t(\"workflow_node.deploy.form.local_shell_env.placeholder\")),\n      preCommand: z\n        .string()\n        .max(20480, t(\"common.errmsg.string_max\", { max: 20480 }))\n        .nullish(),\n      postCommand: z\n        .string()\n        .max(20480, t(\"common.errmsg.string_max\", { max: 20480 }))\n        .nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.format) {\n        case FORMAT_PEM:\n          {\n            if (!values.keyPath?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.local_key_path.placeholder\"),\n                path: [\"keyPath\"],\n              });\n            }\n          }\n          break;\n\n        case FORMAT_PFX:\n          {\n            if (!values.pfxPassword?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.local_pfx_password.placeholder\"),\n                path: [\"pfxPassword\"],\n              });\n            }\n          }\n          break;\n\n        case FORMAT_JKS:\n          {\n            if (!values.jksAlias?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.local_jks_alias.placeholder\"),\n                path: [\"jksAlias\"],\n              });\n            }\n\n            if (!values.jksKeypass?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.local_jks_keypass.placeholder\"),\n                path: [\"jksKeypass\"],\n              });\n            }\n\n            if (!values.jksStorepass?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.local_jks_storepass.placeholder\"),\n                path: [\"jksStorepass\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderLocal, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderMohuaMVH.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\r\nimport { Form, Input } from \"antd\";\r\nimport { createSchemaFieldRule } from \"antd-zod\";\r\nimport { z } from \"zod\";\r\n\r\nimport { useFormNestedFieldsContext } from \"./_context\";\r\n\r\nconst BizDeployNodeConfigFieldsProviderMohuaMVH = () => {\r\n  const { i18n, t } = useTranslation();\r\n\r\n  const { parentNamePath } = useFormNestedFieldsContext();\r\n  const formSchema = z.object({\r\n    [parentNamePath]: getSchema({ i18n }),\r\n  });\r\n  const formRule = createSchemaFieldRule(formSchema);\r\n  const initialValues = getInitialValues();\r\n\r\n  return (\r\n    <>\r\n      <Form.Item\r\n        name={[parentNamePath, \"hostId\"]}\r\n        initialValue={initialValues.hostId}\r\n        label={t(\"workflow_node.deploy.form.mohua_mvh_host_id.label\")}\r\n        rules={[formRule]}\r\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.mohua_mvh_host_id.tooltip\") }}></span>}\r\n      >\r\n        <Input placeholder={t(\"workflow_node.deploy.form.mohua_mvh_host_id.placeholder\")} />\r\n      </Form.Item>\r\n\r\n      <Form.Item\r\n        name={[parentNamePath, \"domainId\"]}\r\n        initialValue={initialValues.domainId}\r\n        label={t(\"workflow_node.deploy.form.mohua_mvh_domain_id.label\")}\r\n        rules={[formRule]}\r\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.mohua_mvh_domain_id.tooltip\") }}></span>}\r\n      >\r\n        <Input placeholder={t(\"workflow_node.deploy.form.mohua_mvh_domain_id.placeholder\")} />\r\n      </Form.Item>\r\n    </>\r\n  );\r\n};\r\n\r\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\r\n  return {\r\n    hostId: \"\",\r\n    domainId: \"\",\r\n  };\r\n};\r\n\r\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\r\n  const { t } = i18n;\r\n\r\n  return z.object({\r\n    hostId: z.union([\r\n      z.string().nonempty(t(\"workflow_node.deploy.form.mohua_mvh_host_id.placeholder\")),\r\n      z.number().int(t(\"workflow_node.deploy.form.mohua_mvh_host_id.placeholder\")),\r\n    ]),\r\n    domainId: z.union([\r\n      z.string().nonempty(t(\"workflow_node.deploy.form.mohua_mvh_domain_id.placeholder\")),\r\n      z.number().int(t(\"workflow_node.deploy.form.mohua_mvh_domain_id.placeholder\")),\r\n    ]),\r\n  });\r\n};\r\n\r\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderMohuaMVH, {\r\n  getInitialValues,\r\n  getSchema,\r\n});\r\n\r\nexport default _default;\r\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderNetlify.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_WEBSITE = \"website\" as const;\n\nconst BizDeployNodeConfigFieldsProviderNetlify = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_WEBSITE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.netlify_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_WEBSITE}>\n        <Form.Item\n          name={[parentNamePath, \"siteId\"]}\n          initialValue={initialValues.siteId}\n          label={t(\"workflow_node.deploy.form.netlify_site_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.netlify_site_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.netlify_site_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceType: RESOURCE_TYPE_WEBSITE,\n    siteId: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      resourceType: z.literal(RESOURCE_TYPE_WEBSITE, t(\"workflow_node.deploy.form.cpanel_resource_type.placeholder\")),\n      siteId: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_WEBSITE:\n          {\n            const scSiteId = z.string().nonempty();\n            if (!scSiteId.safeParse(values.siteId).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.netlify_site_id.placeholder\"),\n                path: [\"siteId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderNetlify, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderNginxProxyManager.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_HOST = \"host\" as const;\nconst RESOURCE_TYPE_CERTIFICATE = \"certificate\" as const;\n\nconst HOST_MATCH_PATTERN_SPECIFIED = \"specified\" as const;\nconst HOST_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst HOST_TYPE_PROXY = \"proxy\" as const;\nconst HOST_TYPE_REDIRECTION = \"redirection\" as const;\nconst HOST_TYPE_STREAM = \"stream\" as const;\nconst HOST_TYPE_DEAD = \"dead\" as const;\n\nconst BizDeployNodeConfigFieldsProviderNginxProxyManager = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n  const fieldHostMatchPattern = Form.useWatch([parentNamePath, \"hostMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_HOST, RESOURCE_TYPE_CERTIFICATE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.nginxproxymanager_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_HOST}>\n        <Form.Item\n          name={[parentNamePath, \"hostMatchPattern\"]}\n          initialValue={initialValues.hostMatchPattern}\n          label={t(\"workflow_node.deploy.form.nginxproxymanager_host_match_pattern.label\")}\n          rules={[formRule]}\n        >\n          <Radio.Group\n            options={[HOST_MATCH_PATTERN_SPECIFIED, HOST_MATCH_PATTERN_CERTSAN].map((s) => ({\n              key: s,\n              label: t(`workflow_node.deploy.form.nginxproxymanager_host_match_pattern.option.${s}.label`),\n              value: s,\n            }))}\n          />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"hostType\"]}\n          initialValue={initialValues.hostId}\n          label={t(\"workflow_node.deploy.form.nginxproxymanager_host_type.label\")}\n          rules={[formRule]}\n        >\n          <Select\n            options={[HOST_TYPE_PROXY, HOST_TYPE_REDIRECTION, HOST_TYPE_STREAM, HOST_TYPE_DEAD].map((s) => ({\n              value: s,\n              label: t(`workflow_node.deploy.form.nginxproxymanager_host_type.option.${s}.label`),\n            }))}\n            placeholder={t(\"workflow_node.deploy.form.nginxproxymanager_host_type.placeholder\")}\n          />\n        </Form.Item>\n\n        <Show when={fieldHostMatchPattern !== HOST_MATCH_PATTERN_CERTSAN}>\n          <Form.Item\n            name={[parentNamePath, \"hostId\"]}\n            initialValue={initialValues.hostId}\n            label={t(\"workflow_node.deploy.form.nginxproxymanager_host_id.label\")}\n            rules={[formRule]}\n            tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.nginxproxymanager_host_id.tooltip\") }}></span>}\n          >\n            <Input type=\"number\" placeholder={t(\"workflow_node.deploy.form.nginxproxymanager_host_id.placeholder\")} />\n          </Form.Item>\n        </Show>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>\n        <Form.Item\n          name={[parentNamePath, \"certificateId\"]}\n          initialValue={initialValues.certificateId}\n          label={t(\"workflow_node.deploy.form.nginxproxymanager_certificate_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.nginxproxymanager_certificate_id.tooltip\") }}></span>}\n        >\n          <Input type=\"number\" placeholder={t(\"workflow_node.deploy.form.nginxproxymanager_certificate_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceType: RESOURCE_TYPE_HOST,\n    hostMatchPattern: HOST_MATCH_PATTERN_SPECIFIED,\n    hostType: HOST_TYPE_PROXY,\n    hostId: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      resourceType: z.literal([RESOURCE_TYPE_HOST, RESOURCE_TYPE_CERTIFICATE], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      hostMatchPattern: z.string().nullish(),\n      hostType: z.string().nullish(),\n      hostId: z.union([z.string(), z.number().int()]).nullish(),\n      certificateId: z.union([z.string(), z.number().int()]).nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_HOST:\n          {\n            if (values.hostMatchPattern) {\n              switch (values.hostMatchPattern) {\n                case HOST_MATCH_PATTERN_SPECIFIED:\n                  {\n                    const scHostType = z.string().nonempty();\n                    if (!scHostType.safeParse(values.hostType).success) {\n                      ctx.addIssue({\n                        code: \"custom\",\n                        message: t(\"workflow_node.deploy.form.nginxproxymanager_host_type.placeholder\"),\n                        path: [\"hostType\"],\n                      });\n                    }\n\n                    const scHostId = z.coerce.number().int().positive();\n                    if (!scHostId.safeParse(values.hostId).success) {\n                      ctx.addIssue({\n                        code: \"custom\",\n                        message: t(\"workflow_node.deploy.form.nginxproxymanager_host_id.placeholder\"),\n                        path: [\"hostId\"],\n                      });\n                    }\n                  }\n                  break;\n              }\n            } else {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.nginxproxymanager_host_match_pattern.placeholder\"),\n                path: [\"hostMatchPattern\"],\n              });\n            }\n          }\n          break;\n\n        case RESOURCE_TYPE_CERTIFICATE:\n          {\n            const scCertificateId = z.coerce.number().int().positive();\n            if (!scCertificateId.safeParse(values.certificateId).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.nginxproxymanager_certificate_id.placeholder\"),\n                path: [\"hostId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderNginxProxyManager, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderProxmoxVE.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderProxmoxVE = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.proxmoxve.guide\") }}></span>} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"nodeName\"]}\n        initialValue={initialValues.nodeName}\n        label={t(\"workflow_node.deploy.form.proxmoxve_node_name.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.proxmoxve_node_name.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"autoRestart\"]}\n        initialValue={initialValues.autoRestart}\n        label={t(\"workflow_node.deploy.form.proxmoxve_auto_restart.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    nodeName: \"\",\n    autoRestart: true,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    nodeName: z.string().nonempty(t(\"workflow_node.deploy.form.proxmoxve_node_name.placeholder\")),\n    autoRestart: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderProxmoxVE, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderQiniuCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderQiniuCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.qiniu_cdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.qiniu_cdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderQiniuCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderQiniuKodo.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderQiniuKodo = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"bucket\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.qiniu_kodo_bucket.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.qiniu_kodo_bucket.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.qiniu_kodo_domain.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.qiniu_kodo_domain.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    bucket: \"\",\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    bucket: z.string().nonempty(t(\"workflow_node.deploy.form.qiniu_kodo_domain.placeholder\")),\n    domain: z.string().refine((v) => isDomain(v), t(\"common.errmsg.domain_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderQiniuKodo, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderQiniuPili.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderQiniuPili = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"hub\"]}\n        initialValue={initialValues.hub}\n        label={t(\"workflow_node.deploy.form.qiniu_pili_hub.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.qiniu_pili_hub.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.qiniu_pili_hub.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.qiniu_pili_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.qiniu_pili_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    hub: \"\",\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      hub: z.string().nonempty(t(\"workflow_node.deploy.form.qiniu_pili_hub.placeholder\")),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n            {\n              if (!isDomain(values.domain!)) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderQiniuPili, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderRainYunRCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\n\nconst BizDeployNodeConfigFieldsProviderRainYunRCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"instanceId\"]}\n        initialValue={initialValues.instanceId}\n        label={t(\"workflow_node.deploy.form.rainyun_rcdn_instance_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.rainyun_rcdn_instance_id.tooltip\") }}></span>}\n      >\n        <Input type=\"number\" placeholder={t(\"workflow_node.deploy.form.rainyun_rcdn_instance_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.rainyun_rcdn_domain.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.rainyun_rcdn_domain.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    instanceId: \"\",\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      instanceId: z.union([z.string(), z.number().int()]).nullish(),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.domainMatchPattern) {\n        case DOMAIN_MATCH_PATTERN_EXACT:\n          {\n            if (!isDomain(values.domain!, { allowWildcard: true })) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"common.errmsg.domain_invalid\"),\n                path: [\"domain\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderRainYunRCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderRainYunSSLCenter.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderRainYunSSLCenter = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"certificateId\"]}\n        initialValue={initialValues.certificateId}\n        label={t(\"workflow_node.deploy.form.rainyun_sslcenter_certificate_id.label\")}\n        extra={t(\"workflow_node.deploy.form.rainyun_sslcenter_certificate_id.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.rainyun_sslcenter_certificate_id.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.rainyun_sslcenter_certificate_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {};\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t: _ } = i18n;\n\n  return z.object({\n    certificateId: z.union([z.string(), z.number().int()]).nullish(),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderRainYunSSLCenter, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderRatPanel.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport MultipleSplitValueInput from \"@/components/MultipleSplitValueInput\";\nimport Show from \"@/components/Show\";\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_WEBSITE = \"website\" as const;\nconst RESOURCE_TYPE_CERTIFICATE = \"certificate\" as const;\n\nconst MULTIPLE_INPUT_SEPARATOR = \";\";\n\nconst BizDeployNodeConfigFieldsProviderRatPanel = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ratpanel.guide\") }}></span>} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_WEBSITE, RESOURCE_TYPE_CERTIFICATE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.ratpanel_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_WEBSITE}>\n        <Form.Item\n          name={[parentNamePath, \"siteNames\"]}\n          initialValue={initialValues.siteNames}\n          label={t(\"workflow_node.deploy.form.ratpanel_site_names.label\")}\n          extra={t(\"workflow_node.deploy.form.ratpanel_site_names.help\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ratpanel_site_names.tooltip\") }}></span>}\n        >\n          <MultipleSplitValueInput\n            modalTitle={t(\"workflow_node.deploy.form.ratpanel_site_names.multiple_input_modal.title\")}\n            placeholder={t(\"workflow_node.deploy.form.ratpanel_site_names.placeholder\")}\n            placeholderInModal={t(\"workflow_node.deploy.form.ratpanel_site_names.multiple_input_modal.placeholder\")}\n            separator={MULTIPLE_INPUT_SEPARATOR}\n            splitOptions={{ removeEmpty: true, trimSpace: true }}\n          />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>\n        <Form.Item\n          name={[parentNamePath, \"certificateId\"]}\n          initialValue={initialValues.certificateId}\n          label={t(\"workflow_node.deploy.form.ratpanel_certificate_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ratpanel_certificate_id.tooltip\") }}></span>}\n        >\n          <Input type=\"number\" placeholder={t(\"workflow_node.deploy.form.ratpanel_certificate_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceType: RESOURCE_TYPE_WEBSITE,\n    siteNames: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      resourceType: z.literal([RESOURCE_TYPE_WEBSITE, RESOURCE_TYPE_CERTIFICATE], t(\"workflow_node.deploy.form.cpanel_resource_type.placeholder\")),\n      siteNames: z.string().nullish(),\n      certificateId: z.union([z.string(), z.number().int()]).nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_WEBSITE:\n          {\n            const scSiteNames = z\n              .string()\n              .nonempty()\n              .refine((v) => {\n                if (!v) return false;\n                return v.split(MULTIPLE_INPUT_SEPARATOR).every((s) => !!s.trim());\n              });\n            if (!scSiteNames.safeParse(values.siteNames).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.ratpanel_site_names.placeholder\"),\n                path: [\"siteNames\"],\n              });\n            }\n          }\n          break;\n\n        case RESOURCE_TYPE_CERTIFICATE:\n          {\n            const scCertificateId = z.coerce.number().int().positive();\n            if (!scCertificateId.safeParse(values.certificateId).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.ratpanel_certificate_id.placeholder\"),\n                path: [\"certificateId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderRatPanel, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderS3.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { CERTIFICATE_FORMATS } from \"@/domain/certificate\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst FORMAT_PEM = CERTIFICATE_FORMATS.PEM;\nconst FORMAT_PFX = CERTIFICATE_FORMATS.PFX;\nconst FORMAT_JKS = CERTIFICATE_FORMATS.JKS;\n\nconst BizDeployNodeConfigFieldsProviderS3 = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldFormat = Form.useWatch([parentNamePath, \"format\"], formInst);\n  const fieldCertPath = Form.useWatch([parentNamePath, \"certObjectKey\"], formInst);\n\n  const handleFormatSelect = (value: string) => {\n    if (fieldFormat === value) return;\n\n    switch (value) {\n      case FORMAT_PEM:\n        {\n          if (/(.pfx|.jks)$/.test(fieldCertPath)) {\n            formInst.setFieldValue([parentNamePath, \"certObjectKey\"], fieldCertPath.replace(/(.pfx|.jks)$/, \".crt\"));\n          }\n        }\n        break;\n\n      case FORMAT_PFX:\n        {\n          if (/(.crt|.jks)$/.test(fieldCertPath)) {\n            formInst.setFieldValue([parentNamePath, \"certObjectKey\"], fieldCertPath.replace(/(.crt|.jks)$/, \".pfx\"));\n          }\n        }\n        break;\n\n      case FORMAT_JKS:\n        {\n          if (/(.crt|.pfx)$/.test(fieldCertPath)) {\n            formInst.setFieldValue([parentNamePath, \"certObjectKey\"], fieldCertPath.replace(/(.crt|.pfx)$/, \".jks\"));\n          }\n        }\n        break;\n    }\n  };\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.s3_region.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.s3_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"bucket\"]}\n        initialValue={initialValues.bucket}\n        label={t(\"workflow_node.deploy.form.s3_bucket.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.s3_bucket.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"format\"]}\n        initialValue={initialValues.format}\n        label={t(\"workflow_node.deploy.form.s3_format.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[FORMAT_PEM, FORMAT_PFX, FORMAT_JKS].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.s3_format.option.${s.toLowerCase()}.label`),\n            value: s,\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.s3_format.placeholder\")}\n          onSelect={handleFormatSelect}\n        />\n      </Form.Item>\n\n      <Show when={fieldFormat === FORMAT_PEM}>\n        <Form.Item\n          name={[parentNamePath, \"keyObjectKey\"]}\n          initialValue={initialValues.keyObjectKey}\n          label={t(\"workflow_node.deploy.form.s3_key_object_key.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.s3_key_object_key.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Form.Item\n        name={[parentNamePath, \"certObjectKey\"]}\n        initialValue={initialValues.certObjectKey}\n        label={t(`workflow_node.deploy.form.s3_${fieldFormat === FORMAT_PEM ? \"fullchaincert\" : \"cert\"}_object_key.label`)}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(`workflow_node.deploy.form.s3_${fieldFormat === FORMAT_PEM ? \"fullchaincert\" : \"cert\"}_object_key.placeholder`)} />\n      </Form.Item>\n\n      <Show when={fieldFormat === FORMAT_PEM}>\n        <Form.Item\n          name={[parentNamePath, \"certObjectKeyForServerOnly\"]}\n          initialValue={initialValues.certObjectKeyForServerOnly}\n          label={t(\"workflow_node.deploy.form.s3_servercert_object_key.label\")}\n          extra={t(\"workflow_node.deploy.form.s3_servercert_object_key.help\")}\n          rules={[formRule]}\n        >\n          <Input allowClear placeholder={t(\"workflow_node.deploy.form.s3_servercert_object_key.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"certObjectKeyForIntermediaOnly\"]}\n          initialValue={initialValues.certObjectKeyForIntermediaOnly}\n          label={t(\"workflow_node.deploy.form.s3_intermediacert_object_key.label\")}\n          extra={t(\"workflow_node.deploy.form.s3_intermediacert_object_key.help\")}\n          rules={[formRule]}\n        >\n          <Input allowClear placeholder={t(\"workflow_node.deploy.form.s3_intermediacert_object_key.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldFormat === FORMAT_PFX}>\n        <Form.Item\n          name={[parentNamePath, \"pfxPassword\"]}\n          initialValue={initialValues.pfxPassword}\n          label={t(\"workflow_node.deploy.form.s3_pfx_password.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.s3_pfx_password.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.s3_pfx_password.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldFormat === FORMAT_JKS}>\n        <Form.Item\n          name={[parentNamePath, \"jksAlias\"]}\n          initialValue={initialValues.jksAlias}\n          label={t(\"workflow_node.deploy.form.s3_jks_alias.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.s3_jks_alias.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.s3_jks_alias.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"jksKeypass\"]}\n          initialValue={initialValues.jksKeypass}\n          label={t(\"workflow_node.deploy.form.s3_jks_keypass.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.s3_jks_keypass.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.s3_jks_keypass.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"jksStorepass\"]}\n          initialValue={initialValues.jksStorepass}\n          label={t(\"workflow_node.deploy.form.s3_jks_storepass.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.s3_jks_storepass.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.s3_jks_storepass.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    bucket: \"\",\n    format: FORMAT_PEM,\n    keyObjectKey: \".certimate/cert.key\",\n    certObjectKey: \".certimate/cert.crt\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.s3_region.placeholder\")),\n      bucket: z.string().nonempty(t(\"workflow_node.deploy.form.s3_bucket.placeholder\")),\n      format: z.literal([FORMAT_PEM, FORMAT_PFX, FORMAT_JKS], t(\"workflow_node.deploy.form.s3_format.placeholder\")),\n      keyObjectKey: z\n        .string()\n        .max(256, t(\"common.errmsg.string_max\", { max: 256 }))\n        .nullish(),\n      certObjectKey: z\n        .string()\n        .min(1, t(\"workflow_node.deploy.form.s3_cert_object_key.placeholder\"))\n        .max(256, t(\"common.errmsg.string_max\", { max: 256 })),\n      certObjectKeyForServerOnly: z\n        .string()\n        .max(256, t(\"common.errmsg.string_max\", { max: 256 }))\n        .nullish(),\n      certObjectKeyForIntermediaOnly: z\n        .string()\n        .max(256, t(\"common.errmsg.string_max\", { max: 256 }))\n        .nullish(),\n      pfxPassword: z.string().nullish(),\n      jksAlias: z.string().nullish(),\n      jksKeypass: z.string().nullish(),\n      jksStorepass: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.format) {\n        case FORMAT_PEM:\n          {\n            if (!values.keyObjectKey?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.s3_key_object_key.placeholder\"),\n                path: [\"keyObjectKey\"],\n              });\n            }\n          }\n          break;\n\n        case FORMAT_PFX:\n          {\n            if (!values.pfxPassword?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.s3_pfx_password.placeholder\"),\n                path: [\"pfxPassword\"],\n              });\n            }\n          }\n          break;\n\n        case FORMAT_JKS:\n          {\n            if (!values.jksAlias?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.s3_jks_alias.placeholder\"),\n                path: [\"jksAlias\"],\n              });\n            }\n\n            if (!values.jksKeypass?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.s3_jks_keypass.placeholder\"),\n                path: [\"jksKeypass\"],\n              });\n            }\n\n            if (!values.jksStorepass?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.s3_jks_storepass.placeholder\"),\n                path: [\"jksStorepass\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderS3, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderSSH.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { IconBulb, IconChevronDown } from \"@tabler/icons-react\";\nimport { Button, Divider, Form, Input, Popover, Select, Space, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport CodeTextInput from \"@/components/CodeTextInput\";\nimport PresetScriptTemplatesPopselect from \"@/components/preset/PresetScriptTemplatesPopselect\";\nimport Show from \"@/components/Show\";\nimport { CERTIFICATE_FORMATS } from \"@/domain/certificate\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\nimport { initPresetScript as _initPresetScript } from \"./BizDeployNodeConfigFieldsProviderLocal\";\n\nconst FORMAT_PEM = CERTIFICATE_FORMATS.PEM;\nconst FORMAT_PFX = CERTIFICATE_FORMATS.PFX;\nconst FORMAT_JKS = CERTIFICATE_FORMATS.JKS;\n\nconst initPresetScript = (\n  key: Parameters<typeof _initPresetScript>[0] | \"sh_replace_synologydsm_ssl\" | \"sh_replace_fnos_ssl\" | \"sh_replace_qnap_ssl\",\n  params?: Parameters<typeof _initPresetScript>[1]\n) => {\n  switch (key) {\n    case \"sh_replace_synologydsm_ssl\":\n      return `# *** 需要 root 权限 ***\n# 注意仅支持替换证书，需本身已开启过一次 HTTPS\n# 脚本参考 https://github.com/catchdave/ssl-certs/blob/main/replace_synology_ssl_certs.sh\n\n# 请将以下变量替换为实际值\n$tmpFullchainPath = \"\\${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}\" # 证书文件路径（与表单中保持一致）\n$tmpCertPath = \"\\${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}\" # 服务器证书文件路径（与表单中保持一致）\n$tmpKeyPath = \"\\${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}\" # 私钥文件路径（与表单中保持一致）\n\nDEBUG=1\nerror_exit() { echo \"[ERROR] $1\"; exit 1; }\nwarn() { echo \"[WARN] $1\"; }\ninfo() { echo \"[INFO] $1\"; }\ndebug() { [[ \"\\${DEBUG}\" ]] && echo \"[DEBUG] $1\"; }\n\ncerts_src_dir=\"/usr/syno/etc/certificate/system/default\"\ntarget_cert_dirs=(\n  \"/usr/syno/etc/certificate/system/FQDN\"\n  \"/usr/local/etc/certificate/ScsiTarget/pkg-scsi-plugin-server/\"\n  \"/usr/local/etc/certificate/SynologyDrive/SynologyDrive/\"\n  \"/usr/local/etc/certificate/WebDAVServer/webdav/\"\n  \"/usr/local/etc/certificate/ActiveBackup/ActiveBackup/\"\n  \"/usr/syno/etc/certificate/smbftpd/ftpd/\")\n\n# 获取证书目录\ndefault_dir_name=$(</usr/syno/etc/certificate/_archive/DEFAULT)\nif [[ -n \"$default_dir_name\" ]]; then\n  target_cert_dirs+=(\"/usr/syno/etc/certificate/_archive/\\${default_dir_name}\")\n  debug \"Default cert directory found: '/usr/syno/etc/certificate/_archive/\\${default_dir_name}'\"\nelse\n  warn \"No default directory found. Probably unusual? Check: 'cat /usr/syno/etc/certificate/_archive/DEFAULT'\"\nfi\n\n# 获取反向代理证书目录\nfor proxy in /usr/syno/etc/certificate/ReverseProxy/*/; do\n  debug \"Found proxy dir: \\${proxy}\"\n  target_cert_dirs+=(\"\\${proxy}\")\ndone\n\n[[ \"\\${DEBUG}\" ]] && set -x\n\n# 复制文件\ncp -rf \"$tmpFullchainPath\" \"\\${certs_src_dir}/fullchain.pem\" || error_exit \"Halting because of error moving fullchain file\"\ncp -rf \"$tmpCertPath\" \"\\${certs_src_dir}/cert.pem\" || error_exit \"Halting because of error moving cert file\"\ncp -rf \"$tmpKeyPath\" \"\\${certs_src_dir}/privkey.pem\" || error_exit \"Halting because of error moving privkey file\"\nchown root:root \"\\${certs_src_dir}/\"{privkey,fullchain,cert}.pem || error_exit \"Halting because of error chowning files\"\ninfo \"Certs moved from /tmp & chowned.\"\n\n# 替换证书\nfor target_dir in \"\\${target_cert_dirs[@]}\"; do\n  if [[ ! -d \"$target_dir\" ]]; then\n    debug \"Target cert directory '$target_dir' not found, skipping...\"\n    continue\n  fi\n  info \"Copying certificates to '$target_dir'\"\n  if ! (cp \"\\${certs_src_dir}/\"{privkey,fullchain,cert}.pem \"$target_dir/\" && \\\n    chown root:root \"$target_dir/\"{privkey,fullchain,cert}.pem); then\n      warn \"Error copying or chowning certs to \\${target_dir}\"\n  fi\ndone\n\n# 重启服务\ninfo \"Rebooting all the things...\"\n/usr/syno/bin/synosystemctl restart nmbd\n/usr/syno/bin/synosystemctl restart avahi\n/usr/syno/bin/synosystemctl restart ldap-server\n/usr/syno/bin/synopkg is_onoff ScsiTarget 1>/dev/null && /usr/syno/bin/synopkg restart ScsiTarget\n/usr/syno/bin/synopkg is_onoff SynologyDrive 1>/dev/null && /usr/syno/bin/synopkg restart SynologyDrive\n/usr/syno/bin/synopkg is_onoff WebDAVServer 1>/dev/null && /usr/syno/bin/synopkg restart WebDAVServer\n/usr/syno/bin/synopkg is_onoff ActiveBackup 1>/dev/null && /usr/syno/bin/synopkg restart ActiveBackup\nif ! /usr/syno/bin/synow3tool --gen-all && sudo /usr/syno/bin/synosystemctl restart nginx; then\n  warn \"nginx failed to restart\"\nfi\n\ninfo \"Completed\"\n      `.trim();\n\n    case \"sh_replace_fnos_ssl\":\n      return `# *** 需要 root 权限 ***\n# 注意仅支持替换证书，需本身已开启过一次 HTTPS\n# 脚本参考 https://github.com/lfgyx/fnos_certificate_update/blob/main/src/update_cert.sh\n\n# 请将以下变量替换为实际值\n# 飞牛证书实际存放路径请在 \\`/usr/trim/etc/network_cert_all.conf\\` 中查看，注意不要修改文件名\n$tmpFullchainPath = \"\\${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}\" # 证书文件路径（与表单中保持一致）\n$tmpCertPath = \"\\${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}\" # 服务器证书文件路径（与表单中保持一致）\n$tmpKeyPath = \"\\${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}\" # 私钥文件路径（与表单中保持一致）\n$fnFullchainPath = \"/usr/trim/var/trim_connect/ssls/example.com/1234567890/fullchain.crt\" # 飞牛证书文件路径\n$fnCertPath = \"/usr/trim/var/trim_connect/ssls/example.com/1234567890/example.com.crt\" # 飞牛服务器证书文件路径\n$fnKeyPath = \"/usr/trim/var/trim_connect/ssls/example.com/1234567890/example.com.key\" # 飞牛私钥文件路径\n$domain = \"<your-domain-name>\" # 域名\n\n# 复制文件\ncp -rf \"$tmpFullchainPath\" \"$fnFullchainPath\"\ncp -rf \"$tmpCertPath\" \"$fnCertPath\"\ncp -rf \"$tmpKeyPath\" \"$fnKeyPath\"\nchmod 755 \"$fnFullchainPath\"\nchmod 755 \"$fnCertPath\"\nchmod 755 \"$fnKeyPath\"\n\n# 更新数据库\nNEW_EFFECT_DATE=$(openssl x509 -startdate -noout -in \"$fnCertPath\" | sed \"s/^.*=\\\\(.*\\\\)$/\\\\1/\")\nNEW_EFFECT_TIMESTAMP=$(date -d \"$NEW_EFFECT_DATE\" +%s%3N)\nNEW_EXPIRY_DATE=$(openssl x509 -enddate -noout -in \"$fnCertPath\" | sed \"s/^.*=\\\\(.*\\\\)$/\\\\1/\")\nNEW_EXPIRY_TIMESTAMP=$(date -d \"$NEW_EXPIRY_DATE\" +%s%3N)\npsql -U postgres -d trim_connect -c \"UPDATE cert SET valid_from=$NEW_EFFECT_TIMESTAMP, valid_to=$NEW_EXPIRY_TIMESTAMP WHERE domain='$domain'\"\n\n# 重启服务\nsystemctl restart webdav.service\nsystemctl restart smbftpd.service\nsystemctl restart trim_nginx.service\n      `.trim();\n\n    case \"sh_replace_qnap_ssl\":\n      return `# *** 需要 root 权限 ***\n# 注意仅支持替换证书，需本身已开启过一次 HTTPS\n\n# 请将以下变量替换为实际值\n$tmpFullchainPath = \"\\${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}\"}\" # 证书文件路径（与表单中保持一致）\n$tmpKeyPath = \"\\${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}\" # 私钥文件路径（与表单中保持一致）\n\n# 复制文件\ncp -rf \"$tmpFullchainPath\" /etc/stunnel/backup.cert\ncp -rf \"$tmpKeyPath\" /etc/stunnel/backup.key\ncat /etc/stunnel/backup.key > /etc/stunnel/stunnel.pem\ncat /etc/stunnel/backup.cert >> /etc/stunnel/stunnel.pem\nchmod 600 /etc/stunnel/backup.cert\nchmod 600 /etc/stunnel/backup.key\nchmod 600 /etc/stunnel/stunnel.pem\n\n# 重启服务\n/etc/init.d/stunnel.sh restart\n/etc/init.d/reverse_proxy.sh reload\n      `.trim();\n  }\n\n  return _initPresetScript(key as Parameters<typeof _initPresetScript>[0], params);\n};\n\nconst BizDeployNodeConfigFieldsProviderSSH = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldFormat = Form.useWatch([parentNamePath, \"format\"], formInst);\n  const fieldCertPath = Form.useWatch([parentNamePath, \"certPath\"], formInst);\n\n  const handleFormatSelect = (value: string) => {\n    if (fieldFormat === value) return;\n\n    switch (value) {\n      case FORMAT_PEM:\n        {\n          if (/(.pfx|.jks)$/.test(fieldCertPath)) {\n            formInst.setFieldValue([parentNamePath, \"certPath\"], fieldCertPath.replace(/(.pfx|.jks)$/, \".crt\"));\n          }\n        }\n        break;\n\n      case FORMAT_PFX:\n        {\n          if (/(.crt|.jks)$/.test(fieldCertPath)) {\n            formInst.setFieldValue([parentNamePath, \"certPath\"], fieldCertPath.replace(/(.crt|.jks)$/, \".pfx\"));\n          }\n        }\n        break;\n\n      case FORMAT_JKS:\n        {\n          if (/(.crt|.pfx)$/.test(fieldCertPath)) {\n            formInst.setFieldValue([parentNamePath, \"certPath\"], fieldCertPath.replace(/(.crt|.pfx)$/, \".jks\"));\n          }\n        }\n        break;\n    }\n  };\n\n  const handlePresetPreScriptClick = (key: string) => {\n    switch (key) {\n      case \"sh_backup_files\":\n      case \"ps_backup_files\":\n        {\n          const presetScriptParams = {\n            certPath: formInst.getFieldValue([parentNamePath, \"certPath\"]),\n            keyPath: formInst.getFieldValue([parentNamePath, \"keyPath\"]),\n          };\n          formInst.setFieldValue([parentNamePath, \"preCommand\"], initPresetScript(key, presetScriptParams));\n        }\n        break;\n    }\n  };\n\n  const handlePresetPostScriptClick = (key: string) => {\n    switch (key) {\n      case \"sh_reload_nginx\":\n        {\n          formInst.setFieldValue([parentNamePath, \"postCommand\"], initPresetScript(key));\n        }\n        break;\n\n      case \"sh_replace_synologydsm_ssl\":\n      case \"sh_replace_fnos_ssl\":\n      case \"sh_replace_qnap_ssl\":\n        {\n          const presetScriptParams = {\n            certPath: formInst.getFieldValue([parentNamePath, \"certPath\"]),\n            certPathForServerOnly: formInst.getFieldValue([parentNamePath, \"certPathForServerOnly\"]),\n            certPathForIntermediaOnly: formInst.getFieldValue([parentNamePath, \"certPathForIntermediaOnly\"]),\n            keyPath: formInst.getFieldValue([parentNamePath, \"keyPath\"]),\n          };\n          formInst.setFieldValue([parentNamePath, \"postCommand\"], initPresetScript(key, presetScriptParams));\n        }\n        break;\n\n      case \"ps_binding_iis\":\n      case \"ps_binding_netsh\":\n      case \"ps_binding_rdp\":\n        {\n          const presetScriptParams = {\n            certPath: formInst.getFieldValue([parentNamePath, \"certPath\"]),\n            pfxPassword: formInst.getFieldValue([parentNamePath, \"pfxPassword\"]),\n          };\n          formInst.setFieldValue([parentNamePath, \"postCommand\"], initPresetScript(key, presetScriptParams));\n        }\n        break;\n    }\n  };\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"useSCP\"]}\n        initialValue={initialValues.useSCP}\n        label={t(\"workflow_node.deploy.form.ssh_use_scp.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ssh_use_scp.tooltip\") }}></span>}\n      >\n        <Switch />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"format\"]}\n        initialValue={initialValues.format}\n        label={t(\"workflow_node.deploy.form.ssh_format.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[FORMAT_PEM, FORMAT_PFX, FORMAT_JKS].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.ssh_format.option.${s.toLowerCase()}.label`),\n            value: s,\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.ssh_format.placeholder\")}\n          onSelect={handleFormatSelect}\n        />\n      </Form.Item>\n\n      <Show when={fieldFormat === FORMAT_PEM}>\n        <Form.Item\n          name={[parentNamePath, \"keyPath\"]}\n          initialValue={initialValues.keyPath}\n          label={t(\"workflow_node.deploy.form.ssh_key_path.label\")}\n          extra={t(\"workflow_node.deploy.form.ssh_key_path.help\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.ssh_key_path.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Form.Item\n        name={[parentNamePath, \"certPath\"]}\n        initialValue={initialValues.certPath}\n        label={t(`workflow_node.deploy.form.ssh_${fieldFormat === FORMAT_PEM ? \"fullchaincert\" : \"cert\"}_path.label`)}\n        extra={t(\"workflow_node.deploy.form.ssh_cert_path.help\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(`workflow_node.deploy.form.ssh_${fieldFormat === FORMAT_PEM ? \"fullchaincert\" : \"cert\"}_path.placeholder`)} />\n      </Form.Item>\n\n      <Show when={fieldFormat === FORMAT_PEM}>\n        <Form.Item\n          name={[parentNamePath, \"certPathForServerOnly\"]}\n          initialValue={initialValues.certPathForServerOnly}\n          label={t(\"workflow_node.deploy.form.ssh_servercert_path.label\")}\n          extra={t(\"workflow_node.deploy.form.ssh_servercert_path.help\")}\n          rules={[formRule]}\n        >\n          <Input allowClear placeholder={t(\"workflow_node.deploy.form.ssh_servercert_path.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"certPathForIntermediaOnly\"]}\n          initialValue={initialValues.certPathForIntermediaOnly}\n          label={t(\"workflow_node.deploy.form.ssh_intermediacert_path.label\")}\n          extra={t(\"workflow_node.deploy.form.ssh_intermediacert_path.help\")}\n          rules={[formRule]}\n        >\n          <Input allowClear placeholder={t(\"workflow_node.deploy.form.ssh_intermediacert_path.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldFormat === FORMAT_PFX}>\n        <Form.Item\n          name={[parentNamePath, \"pfxPassword\"]}\n          initialValue={initialValues.pfxPassword}\n          label={t(\"workflow_node.deploy.form.ssh_pfx_password.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ssh_pfx_password.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.ssh_pfx_password.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldFormat === FORMAT_JKS}>\n        <Form.Item\n          name={[parentNamePath, \"jksAlias\"]}\n          initialValue={initialValues.jksAlias}\n          label={t(\"workflow_node.deploy.form.ssh_jks_alias.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ssh_jks_alias.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.ssh_jks_alias.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"jksKeypass\"]}\n          initialValue={initialValues.jksKeypass}\n          label={t(\"workflow_node.deploy.form.ssh_jks_keypass.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ssh_jks_keypass.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.ssh_jks_keypass.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item\n          name={[parentNamePath, \"jksStorepass\"]}\n          initialValue={initialValues.jksStorepass}\n          label={t(\"workflow_node.deploy.form.ssh_jks_storepass.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ssh_jks_storepass.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.ssh_jks_storepass.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Form.Item label={t(\"workflow_node.deploy.form.ssh_pre_command.label\")}>\n        <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n          <PresetScriptTemplatesPopselect\n            options={[\"sh_backup_files\", \"ps_backup_files\"].map((key) => ({\n              key,\n              label: t(`workflow_node.deploy.form.ssh_preset_scripts.${key}`),\n            }))}\n            trigger={[\"click\"]}\n            onSelect={(key, template) => {\n              if (template) {\n                formInst.setFieldValue([parentNamePath, \"preCommand\"], template.command);\n              } else {\n                handlePresetPreScriptClick(key);\n              }\n            }}\n          >\n            <Button size=\"small\" type=\"link\">\n              {t(\"preset.dropdown.script.button\")}\n              <IconChevronDown size=\"1.25em\" />\n            </Button>\n          </PresetScriptTemplatesPopselect>\n        </div>\n        <Form.Item name={[parentNamePath, \"preCommand\"]} initialValue={initialValues.preCommand} noStyle rules={[formRule]}>\n          <CodeTextInput\n            height=\"auto\"\n            minHeight=\"64px\"\n            maxHeight=\"256px\"\n            language={[\"shell\", \"powershell\"]}\n            placeholder={t(\"workflow_node.deploy.form.ssh_pre_command.placeholder\")}\n          />\n        </Form.Item>\n      </Form.Item>\n\n      <Form.Item label={t(\"workflow_node.deploy.form.ssh_post_command.label\")}>\n        <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n          <Space align=\"center\" separator={<Divider orientation=\"vertical\" />} size={0}>\n            <Popover content={<div dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_script_command.vartips\") }} />} mouseEnterDelay={1}>\n              <Button color=\"default\" size=\"small\" variant=\"link\">\n                <IconBulb size=\"1.25em\" />\n              </Button>\n            </Popover>\n            <PresetScriptTemplatesPopselect\n              options={[\n                \"sh_reload_nginx\",\n                \"sh_replace_synologydsm_ssl\",\n                \"sh_replace_fnos_ssl\",\n                \"sh_replace_qnap_ssl\",\n                \"ps_binding_iis\",\n                \"ps_binding_netsh\",\n                \"ps_binding_rdp\",\n              ].map((key) => ({\n                key,\n                label: t(`workflow_node.deploy.form.ssh_preset_scripts.${key}`),\n              }))}\n              trigger={[\"click\"]}\n              onSelect={(key, template) => {\n                if (template) {\n                  formInst.setFieldValue([parentNamePath, \"postCommand\"], template.command);\n                } else {\n                  handlePresetPostScriptClick(key);\n                }\n              }}\n            >\n              <Button size=\"small\" type=\"link\">\n                {t(\"preset.dropdown.script.button\")}\n                <IconChevronDown size=\"1.25em\" />\n              </Button>\n            </PresetScriptTemplatesPopselect>\n          </Space>\n        </div>\n        <Form.Item name={[parentNamePath, \"postCommand\"]} initialValue={initialValues.postCommand} noStyle rules={[formRule]}>\n          <CodeTextInput\n            height=\"auto\"\n            minHeight=\"64px\"\n            maxHeight=\"256px\"\n            language={[\"shell\", \"powershell\"]}\n            placeholder={t(\"workflow_node.deploy.form.ssh_post_command.placeholder\")}\n          />\n        </Form.Item>\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    format: FORMAT_PEM,\n    keyPath: \"/etc/ssl/certimate/cert.key\",\n    certPath: \"/etc/ssl/certimate/cert.crt\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      useSCP: z.boolean().nullish(),\n      format: z.literal([FORMAT_PEM, FORMAT_PFX, FORMAT_JKS], t(\"workflow_node.deploy.form.ssh_format.placeholder\")),\n      keyPath: z\n        .string()\n        .max(256, t(\"common.errmsg.string_max\", { max: 256 }))\n        .nullish(),\n      certPath: z\n        .string()\n        .min(1, t(\"workflow_node.deploy.form.ssh_cert_path.placeholder\"))\n        .max(256, t(\"common.errmsg.string_max\", { max: 256 })),\n      certPathForServerOnly: z\n        .string()\n        .max(256, t(\"common.errmsg.string_max\", { max: 256 }))\n        .nullish(),\n      certPathForIntermediaOnly: z\n        .string()\n        .max(256, t(\"common.errmsg.string_max\", { max: 256 }))\n        .nullish(),\n      pfxPassword: z.string().nullish(),\n      jksAlias: z.string().nullish(),\n      jksKeypass: z.string().nullish(),\n      jksStorepass: z.string().nullish(),\n      preCommand: z\n        .string()\n        .max(20480, t(\"common.errmsg.string_max\", { max: 20480 }))\n        .nullish(),\n      postCommand: z\n        .string()\n        .max(20480, t(\"common.errmsg.string_max\", { max: 20480 }))\n        .nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.format) {\n        case FORMAT_PEM:\n          {\n            if (!values.keyPath?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.ssh_key_path.placeholder\"),\n                path: [\"keyPath\"],\n              });\n            }\n          }\n          break;\n\n        case FORMAT_PFX:\n          {\n            if (!values.pfxPassword?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.ssh_pfx_password.placeholder\"),\n                path: [\"pfxPassword\"],\n              });\n            }\n          }\n          break;\n\n        case FORMAT_JKS:\n          {\n            if (!values.jksAlias?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.ssh_jks_alias.placeholder\"),\n                path: [\"jksAlias\"],\n              });\n            }\n\n            if (!values.jksKeypass?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.ssh_jks_keypass.placeholder\"),\n                path: [\"jksKeypass\"],\n              });\n            }\n\n            if (!values.jksStorepass?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.ssh_jks_storepass.placeholder\"),\n                path: [\"jksStorepass\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderSSH, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderSafeLine.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_CERTIFICATE = \"certificate\" as const;\n\nconst BizDeployNodeConfigFieldsProviderSafeLine = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.safeline.guide\") }}></span>} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_CERTIFICATE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.safeline_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>\n        <Form.Item\n          name={[parentNamePath, \"certificateId\"]}\n          initialValue={initialValues.certificateId}\n          label={t(\"workflow_node.deploy.form.safeline_certificate_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.safeline_certificate_id.tooltip\") }}></span>}\n        >\n          <Input type=\"number\" placeholder={t(\"workflow_node.deploy.form.safeline_certificate_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceType: RESOURCE_TYPE_CERTIFICATE,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      certificateId: z.union([z.string(), z.number().int()]).nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_CERTIFICATE:\n          {\n            const scCertificateId = z.coerce.number().int().positive();\n            if (!scCertificateId.safeParse(values.certificateId).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.safeline_certificate_id.placeholder\"),\n                path: [\"certificateId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderSafeLine, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderSynologyDSM.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderSynologyDSM = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.synologydsm.guide\") }}></span>} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"certificateIdOrDesc\"]}\n        initialValue={initialValues.certificateIdOrDesc}\n        label={t(\"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.label\")}\n        extra={t(\"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.help\")}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.tooltip\") }}></span>}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"isDefault\"]}\n        initialValue={initialValues.isDefault}\n        label={t(\"workflow_node.deploy.form.synologydsm_is_default.label\")}\n        rules={[formRule]}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    isDefault: true,\n  };\n};\n\nconst getSchema = ({ i18n: _i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  return z.object({\n    certificateIdOrDesc: z.string().nullish(),\n    isDefault: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderSynologyDSM, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderTencentCloudCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"endpoint\"]}\n        initialValue={initialValues.endpoint}\n        label={t(\"workflow_node.deploy.form.tencentcloud_cdn_endpoint.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_cdn_endpoint.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.tencentcloud_cdn_endpoint.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.tencentcloud_cdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_cdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      endpoint: z.string().nullish(),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudCLB.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_LOADBALANCER = \"loadbalancer\" as const;\nconst RESOURCE_TYPE_LISTENER = \"listener\" as const;\nconst RESOURCE_TYPE_RULEDOMAIN = \"ruledomain\" as const;\n\nconst BizDeployNodeConfigFieldsProviderTencentCloudCLB = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"endpoint\"]}\n        initialValue={initialValues.endpoint}\n        label={t(\"workflow_node.deploy.form.tencentcloud_clb_endpoint.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_clb_endpoint.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.tencentcloud_clb_endpoint.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.tencentcloud_clb_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_clb_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_clb_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER, RESOURCE_TYPE_RULEDOMAIN].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.tencentcloud_clb_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"loadbalancerId\"]}\n        initialValue={initialValues.loadbalancerId}\n        label={t(\"workflow_node.deploy.form.tencentcloud_clb_loadbalancer_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_clb_loadbalancer_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_clb_loadbalancer_id.placeholder\")} />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LISTENER || fieldResourceType === RESOURCE_TYPE_RULEDOMAIN}>\n        <Form.Item\n          name={[parentNamePath, \"listenerId\"]}\n          initialValue={initialValues.listenerId}\n          label={t(\"workflow_node.deploy.form.tencentcloud_clb_listener_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_clb_listener_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_clb_listener_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_RULEDOMAIN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.tencentcloud_clb_ruledomain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_clb_ruledomain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    resourceType: RESOURCE_TYPE_LISTENER,\n    loadbalancerId: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      endpoint: z.string().nullish(),\n      resourceType: z.literal(\n        [RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER, RESOURCE_TYPE_RULEDOMAIN],\n        t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")\n      ),\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.tencentcloud_clb_region.placeholder\")),\n      loadbalancerId: z.string().nonempty(t(\"workflow_node.deploy.form.tencentcloud_clb_loadbalancer_id.placeholder\")),\n      listenerId: z.string().nullish(),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_LISTENER:\n          {\n            if (!values.listenerId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.tencentcloud_clb_listener_id.placeholder\"),\n                path: [\"listenerId\"],\n              });\n            }\n          }\n          break;\n\n        case RESOURCE_TYPE_RULEDOMAIN:\n          {\n            if (!values.listenerId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.tencentcloud_clb_listener_id.placeholder\"),\n                path: [\"listenerId\"],\n              });\n            }\n\n            if (!isDomain(values.domain!, { allowWildcard: true })) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"common.errmsg.domain_invalid\"),\n                path: [\"domain\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudCLB, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudCOS.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderTencentCloudCOS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.tencentcloud_cos_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_cos_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_cos_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"bucket\"]}\n        initialValue={initialValues.bucket}\n        label={t(\"workflow_node.deploy.form.tencentcloud_cos_bucket.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_cos_bucket.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.tencentcloud_cos_domain.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_cos_domain.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    bucket: \"\",\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.deploy.form.tencentcloud_cos_region.placeholder\")),\n    bucket: z.string().nonempty(t(\"workflow_node.deploy.form.tencentcloud_cos_bucket.placeholder\")),\n    domain: z.string().refine((v) => isDomain(v), t(\"common.errmsg.domain_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudCOS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudCSS.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderTencentCloudCSS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"endpoint\"]}\n        initialValue={initialValues.endpoint}\n        label={t(\"workflow_node.deploy.form.tencentcloud_css_endpoint.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_css_endpoint.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.tencentcloud_css_endpoint.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.tencentcloud_css_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_css_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      endpoint: z.string().nullish(),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n            {\n              if (!isDomain(values.domain!)) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudCSS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudECDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderTencentCloudECDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"endpoint\"]}\n        initialValue={initialValues.endpoint}\n        label={t(\"workflow_node.deploy.form.tencentcloud_ecdn_endpoint.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_ecdn_endpoint.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.tencentcloud_ecdn_endpoint.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.tencentcloud_ecdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_ecdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      endpoint: z.string().nullish(),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudECDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudEO.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport MultipleSplitValueInput from \"@/components/MultipleSplitValueInput\";\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst MULTIPLE_INPUT_SEPARATOR = \";\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderTencentCloudEO = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"endpoint\"]}\n        initialValue={initialValues.endpoint}\n        label={t(\"workflow_node.deploy.form.tencentcloud_eo_endpoint.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_eo_endpoint.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.tencentcloud_eo_endpoint.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"zoneId\"]}\n        initialValue={initialValues.zoneId}\n        label={t(\"workflow_node.deploy.form.tencentcloud_eo_zone_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_eo_zone_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domains\"]}\n          initialValue={initialValues.domains}\n          label={t(\"workflow_node.deploy.form.tencentcloud_eo_domains.label\")}\n          extra={t(\"workflow_node.deploy.form.tencentcloud_eo_domains.help\")}\n          rules={[formRule]}\n        >\n          <MultipleSplitValueInput\n            modalTitle={t(\"workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.title\")}\n            placeholder={t(\"workflow_node.deploy.form.tencentcloud_eo_domains.placeholder\")}\n            placeholderInModal={t(\"workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.placeholder\")}\n            splitOptions={{ removeEmpty: true, trimSpace: true }}\n          />\n        </Form.Item>\n      </Show>\n\n      <Form.Item\n        label={t(\"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.label\")}\n        extra={t(\"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.help\")}\n      >\n        <span className=\"inline-block\">\n          <Form.Item name={[parentNamePath, \"enableMultipleSSL\"]} initialValue={initialValues.enableMultipleSSL} noStyle rules={[formRule]}>\n            <Switch\n              checkedChildren={t(\"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.on\")}\n              unCheckedChildren={t(\"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.off\")}\n            />\n          </Form.Item>\n        </span>\n        <span className=\"ms-2 inline-block\">{t(\"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.suffix\")}</span>\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    zoneId: \"\",\n    domains: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      endpoint: z.string().nullish(),\n      zoneId: z.string().nonempty(t(\"workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder\")),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domains: z.string().nullish(),\n      enableMultipleSSL: z.boolean().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              const valid = values.domains && values.domains.split(MULTIPLE_INPUT_SEPARATOR).every((e) => isDomain(e, { allowWildcard: true }));\n              if (!valid) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domains\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudEO, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudGAAP.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_LISTENER = \"listener\" as const;\n\nconst BizDeployNodeConfigFieldsProviderTencentCloudGAAP = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"endpoint\"]}\n        initialValue={initialValues.endpoint}\n        label={t(\"workflow_node.deploy.form.tencentcloud_gaap_endpoint.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_gaap_endpoint.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.tencentcloud_gaap_endpoint.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_LISTENER].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.tencentcloud_gaap_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"proxyId\"]}\n        initialValue={initialValues.proxyId}\n        label={t(\"workflow_node.deploy.form.tencentcloud_gaap_proxy_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_gaap_proxy_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_gaap_proxy_id.placeholder\")} />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"listenerId\"]}\n          initialValue={initialValues.listenerId}\n          label={t(\"workflow_node.deploy.form.tencentcloud_gaap_listener_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_gaap_listener_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_gaap_listener_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    resourceType: RESOURCE_TYPE_LISTENER,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      endpoint: z.string().nullish(),\n      resourceType: z.literal(RESOURCE_TYPE_LISTENER, t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      proxyId: z.string().nullish(),\n      listenerId: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_LISTENER:\n          {\n            if (!values.listenerId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.tencentcloud_gaap_listener_id.placeholder\"),\n                path: [\"listenerId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudGAAP, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudSCF.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderTencentCloudSCF = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"endpoint\"]}\n        initialValue={initialValues.endpoint}\n        label={t(\"workflow_node.deploy.form.tencentcloud_scf_endpoint.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_scf_endpoint.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.tencentcloud_scf_endpoint.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.tencentcloud_scf_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_scf_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_scf_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.tencentcloud_scf_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_scf_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      endpoint: z.string().nullish(),\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.tencentcloud_scf_region.placeholder\")),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n            {\n              if (!isDomain(values.domain!)) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudSCF, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudSSL.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderTencentCloudSSL = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"endpoint\"]}\n        initialValue={initialValues.endpoint}\n        label={t(\"workflow_node.deploy.form.tencentcloud_ssl_endpoint.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_ssl_endpoint.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.tencentcloud_ssl_endpoint.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {};\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t: _ } = i18n;\n\n  return z.object({\n    endpoint: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudSSL, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudSSLDeploy.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { AutoComplete, Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport MultipleSplitValueInput from \"@/components/MultipleSplitValueInput\";\nimport Tips from \"@/components/Tips\";\nimport { matchSearchOption } from \"@/utils/search\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst MULTIPLE_INPUT_SEPARATOR = \";\";\n\nconst BizDeployNodeConfigFieldsProviderTencentCloudSSLDeploy = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_ssldeploy.guide\") }}></span>} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"endpoint\"]}\n        initialValue={initialValues.endpoint}\n        label={t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_endpoint.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_endpoint.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_endpoint.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceProduct\"]}\n        initialValue={initialValues.resourceProduct}\n        label={t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_product.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_product.tooltip\") }}></span>}\n      >\n        <AutoComplete\n          options={[\"apigateway\", \"cdn\", \"clb\", \"cos\", \"ddos\", \"lighthouse\", \"live\", \"tcb\", \"teo\", \"tke\", \"tse\", \"vod\", \"waf\"].map((value) => ({ value }))}\n          placeholder={t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_product.placeholder\")}\n          showSearch={{\n            filterOption: (inputValue, option) => matchSearchOption(inputValue, option!),\n          }}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceIds\"]}\n        initialValue={initialValues.resourceIds}\n        label={t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.label\")}\n        extra={t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.tooltip\") }}></span>}\n      >\n        <MultipleSplitValueInput\n          modalTitle={t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.multiple_input_modal.title\")}\n          placeholder={t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.placeholder\")}\n          placeholderInModal={t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.multiple_input_modal.placeholder\")}\n          separator={MULTIPLE_INPUT_SEPARATOR}\n          splitOptions={{ removeEmpty: true, trimSpace: true }}\n        />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    resourceProduct: \"\",\n    resourceIds: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    endpoint: z.string().nullish(),\n    region: z.string().nonempty(t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_region.placeholder\")),\n    resourceProduct: z.string().nonempty(t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_product.placeholder\")),\n    resourceIds: z.string().refine((v) => {\n      if (!v) return false;\n      return v.split(MULTIPLE_INPUT_SEPARATOR).every((e) => /^[A-Za-z0-9*._\\-|]+$/.test(e));\n    }, t(\"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.errmsg.invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudSSLDeploy, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudSSLUpdate.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Switch } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport MultipleSplitValueInput from \"@/components/MultipleSplitValueInput\";\nimport Tips from \"@/components/Tips\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst MULTIPLE_INPUT_SEPARATOR = \";\";\n\nconst BizDeployNodeConfigFieldsProviderTencentCloudSSLUpdate = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_sslupdate.guide\") }}></span>} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"endpoint\"]}\n        initialValue={initialValues.endpoint}\n        label={t(\"workflow_node.deploy.form.tencentcloud_sslupdate_endpoint.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_sslupdate_endpoint.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.tencentcloud_sslupdate_endpoint.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"certificateId\"]}\n        initialValue={initialValues.certificateId}\n        label={t(\"workflow_node.deploy.form.tencentcloud_sslupdate_certificate_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_sslupdate_certificate_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_sslupdate_certificate_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceProducts\"]}\n        initialValue={initialValues.resourceProducts}\n        label={t(\"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.label\")}\n        extra={t(\"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.tooltip\") }}></span>}\n      >\n        <MultipleSplitValueInput\n          modalTitle={t(\"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.multiple_input_modal.title\")}\n          placeholder={t(\"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.placeholder\")}\n          placeholderInModal={t(\"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.multiple_input_modal.placeholder\")}\n          separator={MULTIPLE_INPUT_SEPARATOR}\n          splitOptions={{ removeEmpty: true, trimSpace: true }}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceRegions\"]}\n        initialValue={initialValues.resourceRegions}\n        label={t(\"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.label\")}\n        extra={t(\"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.tooltip\") }}></span>}\n      >\n        <MultipleSplitValueInput\n          modalTitle={t(\"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.multiple_input_modal.title\")}\n          placeholder={t(\"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.placeholder\")}\n          placeholderInModal={t(\"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.multiple_input_modal.placeholder\")}\n          separator={MULTIPLE_INPUT_SEPARATOR}\n          splitOptions={{ removeEmpty: true, trimSpace: true }}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"isReplaced\"]}\n        initialValue={initialValues.isReplaced}\n        label={t(\"workflow_node.deploy.form.tencentcloud_sslupdate_is_replaced.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_sslupdate_is_replaced.tooltip\") }}></span>}\n      >\n        <Switch />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    certificateId: \"\",\n    resourceProducts: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    endpoint: z.string().nullish(),\n    certificateId: z.string().nonempty(t(\"workflow_node.deploy.form.tencentcloud_sslupdate_certificate_id.placeholder\")),\n    resourceProducts: z.string().refine((v) => {\n      if (!v) return false;\n      return v.split(MULTIPLE_INPUT_SEPARATOR).every((e) => !!e.trim());\n    }, t(\"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.placeholder\")),\n    resourceRegions: z\n      .string()\n      .nullish()\n      .refine((v) => {\n        if (!v) return true;\n        return v.split(MULTIPLE_INPUT_SEPARATOR).every((e) => !!e.trim());\n      }, t(\"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.placeholder\")),\n    isReplaced: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudSSLUpdate, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudVOD.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderTencentCloudVOD = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"endpoint\"]}\n        initialValue={initialValues.endpoint}\n        label={t(\"workflow_node.deploy.form.tencentcloud_vod_endpoint.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_vod_endpoint.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.tencentcloud_vod_endpoint.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"subAppId\"]}\n        initialValue={initialValues.subAppId}\n        label={t(\"workflow_node.deploy.form.tencentcloud_vod_sub_app_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_vod_sub_app_id.tooltip\") }}></span>}\n      >\n        <Input type=\"number\" placeholder={t(\"workflow_node.deploy.form.tencentcloud_vod_sub_app_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.tencentcloud_vod_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_vod_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      endpoint: z.string().nullish(),\n      subAppId: z\n        .union([z.string(), z.number().int()])\n        .nullish()\n        .refine((v) => {\n          if (v == null) return true;\n          return /^\\d+$/.test(v + \"\") && +v > 0;\n        }, t(\"workflow_node.deploy.form.tencentcloud_vod_sub_app_id.placeholder\")),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n            {\n              if (!isDomain(values.domain!)) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudVOD, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudWAF.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderTencentCloudWAF = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"endpoint\"]}\n        initialValue={initialValues.endpoint}\n        label={t(\"workflow_node.deploy.form.tencentcloud_waf_endpoint.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_waf_endpoint.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.tencentcloud_waf_endpoint.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.tencentcloud_waf_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_waf_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_waf_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.tencentcloud_waf_domain.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_waf_domain.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainId\"]}\n        initialValue={initialValues.domainId}\n        label={t(\"workflow_node.deploy.form.tencentcloud_waf_domain_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_waf_domain_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_waf_domain_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"instanceId\"]}\n        initialValue={initialValues.instanceId}\n        label={t(\"workflow_node.deploy.form.tencentcloud_waf_instance_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.tencentcloud_waf_instance_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.tencentcloud_waf_instance_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    domain: \"\",\n    domainId: \"\",\n    instanceId: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    endpoint: z.string().nullish(),\n    region: z.string().nonempty(t(\"workflow_node.deploy.form.tencentcloud_waf_region.placeholder\")),\n    domain: z.string().refine((v) => isDomain(v), t(\"common.errmsg.domain_invalid\")),\n    domainId: z.string().nonempty(t(\"workflow_node.deploy.form.tencentcloud_waf_domain_id.placeholder\")),\n    instanceId: z.string().nonempty(t(\"workflow_node.deploy.form.tencentcloud_waf_instance_id.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudWAF, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUCloudUALB.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_LOADBALANCER = \"loadbalancer\" as const;\nconst RESOURCE_TYPE_LISTENER = \"listener\" as const;\n\nconst BizDeployNodeConfigFieldsProviderUCloudUALB = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.ucloud_ualb_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ucloud_ualb_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.ucloud_ualb_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.ucloud_ualb_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"loadbalancerId\"]}\n        initialValue={initialValues.loadbalancerId}\n        label={t(\"workflow_node.deploy.form.ucloud_ualb_loadbalancer_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ucloud_ualb_loadbalancer_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.ucloud_ualb_loadbalancer_id.placeholder\")} />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"listenerId\"]}\n          initialValue={initialValues.listenerId}\n          label={t(\"workflow_node.deploy.form.ucloud_ualb_listener_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ucloud_ualb_listener_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.ucloud_ualb_listener_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER || fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.ucloud_ualb_snidomain.label\")}\n          extra={t(\"workflow_node.deploy.form.ucloud_ualb_snidomain.help\")}\n          rules={[formRule]}\n        >\n          <Input allowClear placeholder={t(\"workflow_node.deploy.form.ucloud_ualb_snidomain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    resourceType: RESOURCE_TYPE_LISTENER,\n    loadbalancerId: \"\",\n    listenerId: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      endpoint: z.string().nullish(),\n      resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.ucloud_ualb_region.placeholder\")),\n      loadbalancerId: z.string().nonempty(t(\"workflow_node.deploy.form.ucloud_ualb_loadbalancer_id.placeholder\")),\n      listenerId: z.string().nullish(),\n      domain: z\n        .string()\n        .nullish()\n        .refine((v) => {\n          if (!v) return true;\n          return isDomain(v);\n        }, t(\"common.errmsg.domain_invalid\")),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_LISTENER:\n          {\n            if (!values.listenerId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.ucloud_ualb_listener_id.placeholder\"),\n                path: [\"listenerId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderUCloudUALB, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUCloudUCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderUCloudUCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domainId\"]}\n        initialValue={initialValues.domainId}\n        label={t(\"workflow_node.deploy.form.ucloud_ucdn_domain_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ucloud_ucdn_domain_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.ucloud_ucdn_domain_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainId: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    domainId: z.string().nonempty(t(\"workflow_node.deploy.form.ucloud_ucdn_domain_id.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderUCloudUCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUCloudUCLB.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_LOADBALANCER = \"loadbalancer\" as const;\nconst RESOURCE_TYPE_VSERVER = \"vserver\" as const;\n\nconst BizDeployNodeConfigFieldsProviderUCloudUCLB = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.ucloud_uclb_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ucloud_uclb_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.ucloud_uclb_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_VSERVER].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.ucloud_uclb_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"loadbalancerId\"]}\n        initialValue={initialValues.loadbalancerId}\n        label={t(\"workflow_node.deploy.form.ucloud_uclb_loadbalancer_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ucloud_uclb_loadbalancer_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.ucloud_uclb_loadbalancer_id.placeholder\")} />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_VSERVER}>\n        <Form.Item\n          name={[parentNamePath, \"vserverId\"]}\n          initialValue={initialValues.vserverId}\n          label={t(\"workflow_node.deploy.form.ucloud_uclb_vserver_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ucloud_uclb_vserver_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.ucloud_uclb_vserver_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    resourceType: RESOURCE_TYPE_VSERVER,\n    loadbalancerId: \"\",\n    vserverId: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      endpoint: z.string().nullish(),\n      resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_VSERVER], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.ucloud_uclb_region.placeholder\")),\n      loadbalancerId: z.string().nonempty(t(\"workflow_node.deploy.form.ucloud_uclb_loadbalancer_id.placeholder\")),\n      vserverId: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_VSERVER:\n          {\n            if (!values.vserverId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.ucloud_uclb_vserver_id.placeholder\"),\n                path: [\"vserverId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderUCloudUCLB, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUCloudUEWAF.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderUCloudUEWAF = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.ucloud_uewaf_domain.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.ucloud_uewaf_domain.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    domain: z.string().refine((v) => isDomain(v), t(\"common.errmsg.domain_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderUCloudUEWAF, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUCloudUPathX.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isPortNumber } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderUCloudUPathX = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"acceleratorId\"]}\n        initialValue={initialValues.acceleratorId}\n        label={t(\"workflow_node.deploy.form.ucloud_upathx_accelerator_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ucloud_upathx_accelerator_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.ucloud_upathx_accelerator_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"listenerPort\"]}\n        initialValue={initialValues.listenerPort}\n        label={t(\"workflow_node.deploy.form.ucloud_upathx_listener_port.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ucloud_upathx_listener_port.tooltip\") }}></span>}\n      >\n        <Input type=\"number\" min={1} max={65535} placeholder={t(\"workflow_node.deploy.form.ucloud_upathx_listener_port.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    acceleratorId: \"\",\n    listenerPort: 443,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    acceleratorId: z.string().nonempty(t(\"workflow_node.deploy.form.ucloud_upathx_accelerator_id.placeholder\")),\n    listenerPort: z.coerce.number().refine((v) => isPortNumber(v), t(\"common.errmsg.port_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderUCloudUPathX, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUCloudUS3.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderUCloudUS3 = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.ucloud_us3_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.ucloud_us3_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.ucloud_us3_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"bucket\"]}\n        initialValue={initialValues.bucket}\n        label={t(\"workflow_node.deploy.form.ucloud_us3_bucket.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.ucloud_us3_bucket.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.ucloud_us3_domain.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.ucloud_us3_domain.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    bucket: \"\",\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.deploy.form.ucloud_us3_region.placeholder\")),\n    bucket: z.string().nonempty(t(\"workflow_node.deploy.form.ucloud_us3_bucket.placeholder\")),\n    domain: z.string().refine((v) => isDomain(v), t(\"common.errmsg.domain_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderUCloudUS3, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUniCloudWebHost.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderUniCloudWebHost = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.unicloud_webhost.guide\") }}></span>} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"spaceProvider\"]}\n        initialValue={initialValues.spaceProvider}\n        label={t(\"workflow_node.deploy.form.unicloud_webhost_space_provider.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[\"aliyun\", \"tencent\"].map((s) => ({\n            label: t(`workflow_node.deploy.form.unicloud_webhost_space_provider.option.${s}.label`),\n            value: s,\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.unicloud_webhost_space_provider.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"spaceId\"]}\n        initialValue={initialValues.spaceId}\n        label={t(\"workflow_node.deploy.form.unicloud_webhost_space_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.unicloud_webhost_space_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.unicloud_webhost_space_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.unicloud_webhost_domain.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.unicloud_webhost_domain.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    spaceProvider: \"tencent\",\n    spaceId: \"\",\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    spaceProvider: z.string().nonempty(t(\"workflow_node.deploy.form.unicloud_webhost_space_provider.placeholder\")),\n    spaceId: z.string().nonempty(t(\"workflow_node.deploy.form.unicloud_webhost_space_id.placeholder\")),\n    domain: z.string().refine((v) => isDomain(v), t(\"common.errmsg.domain_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderUniCloudWebHost, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUpyunCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport Tips from \"@/components/Tips\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderUpyunCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.upyun_cdn.guide\") }}></span>} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.upyun_cdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.upyun_cdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderUpyunCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUpyunFile.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderUpyunFile = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.upyun_file.guide\") }}></span>} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"bucket\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.upyun_file_bucket.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.upyun_file_bucket.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.upyun_file_domain.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.upyun_file_domain.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    bucket: \"\",\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    bucket: z.string().nonempty(t(\"workflow_node.deploy.form.upyun_file_bucket.placeholder\")),\n    domain: z.string().refine((v) => isDomain(v), t(\"common.errmsg.domain_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderUpyunFile, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineALB.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_LOADBALANCER = \"loadbalancer\" as const;\nconst RESOURCE_TYPE_LISTENER = \"listener\" as const;\n\nconst BizDeployNodeConfigFieldsProviderVolcEngineALB = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.volcengine_alb_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.volcengine_alb_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.volcengine_alb_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.volcengine_alb_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER}>\n        <Form.Item\n          name={[parentNamePath, \"loadbalancerId\"]}\n          initialValue={initialValues.loadbalancerId}\n          label={t(\"workflow_node.deploy.form.volcengine_alb_loadbalancer_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.volcengine_alb_loadbalancer_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.volcengine_alb_loadbalancer_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"listenerId\"]}\n          initialValue={initialValues.listenerId}\n          label={t(\"workflow_node.deploy.form.volcengine_alb_listener_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.volcengine_alb_listener_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.volcengine_alb_listener_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER || fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.volcengine_alb_snidomain.label\")}\n          extra={t(\"workflow_node.deploy.form.volcengine_alb_snidomain.help\")}\n          rules={[formRule]}\n        >\n          <Input allowClear placeholder={t(\"workflow_node.deploy.form.volcengine_alb_snidomain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    resourceType: RESOURCE_TYPE_LISTENER,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.volcengine_alb_region.placeholder\")),\n      resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      loadbalancerId: z.string().nullish(),\n      listenerId: z.string().nullish(),\n      domain: z\n        .string()\n        .nullish()\n        .refine((v) => {\n          if (!v) return true;\n          return isDomain(v, { allowWildcard: true });\n        }, t(\"common.errmsg.domain_invalid\")),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_LOADBALANCER:\n          {\n            if (!values.loadbalancerId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.volcengine_alb_loadbalancer_id.placeholder\"),\n                path: [\"loadbalancerId\"],\n              });\n            }\n          }\n          break;\n\n        case RESOURCE_TYPE_LISTENER:\n          {\n            if (!values.listenerId?.trim()) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.volcengine_alb_listener_id.placeholder\"),\n                path: [\"listenerId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineALB, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderVolcEngineCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.volcengine_cdn_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.volcengine_cdn_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineCLB.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst RESOURCE_TYPE_LOADBALANCER = \"loadbalancer\" as const;\nconst RESOURCE_TYPE_LISTENER = \"listener\" as const;\n\nconst BizDeployNodeConfigFieldsProviderVolcEngineCLB = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldResourceType = Form.useWatch([parentNamePath, \"resourceType\"], formInst);\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.volcengine_clb_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.volcengine_clb_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.volcengine_clb_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"resourceType\"]}\n        initialValue={initialValues.resourceType}\n        label={t(\"workflow_node.deploy.form.shared_resource_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.volcengine_clb_resource_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER}>\n        <Form.Item\n          name={[parentNamePath, \"loadbalancerId\"]}\n          initialValue={initialValues.loadbalancerId}\n          label={t(\"workflow_node.deploy.form.volcengine_clb_loadbalancer_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.volcengine_clb_loadbalancer_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.volcengine_clb_loadbalancer_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n\n      <Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>\n        <Form.Item\n          name={[parentNamePath, \"listenerId\"]}\n          initialValue={initialValues.listenerId}\n          label={t(\"workflow_node.deploy.form.volcengine_clb_listener_id.label\")}\n          rules={[formRule]}\n          tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.volcengine_clb_listener_id.tooltip\") }}></span>}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.volcengine_clb_listener_id.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    resourceType: RESOURCE_TYPE_LISTENER,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      region: z.string().nonempty(t(\"workflow_node.deploy.form.volcengine_clb_region.placeholder\")),\n      resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t(\"workflow_node.deploy.form.shared_resource_type.placeholder\")),\n      loadbalancerId: z.string().nullish(),\n      listenerId: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.resourceType) {\n        case RESOURCE_TYPE_LOADBALANCER:\n          {\n            const scLoadbalancerId = z.string().nonempty();\n            if (!scLoadbalancerId.safeParse(values.loadbalancerId).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.volcengine_clb_loadbalancer_id.placeholder\"),\n                path: [\"loadbalancerId\"],\n              });\n            }\n          }\n          break;\n\n        case RESOURCE_TYPE_LISTENER:\n          {\n            const scListenerId = z.string().nonempty();\n            if (!scListenerId.safeParse(values.listenerId).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.deploy.form.volcengine_clb_listener_id.placeholder\"),\n                path: [\"listenerId\"],\n              });\n            }\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineCLB, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineCertCenter.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderVolcEngineCertCenter = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.volcengine_certcenter_region.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.volcengine_certcenter_region.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.deploy.form.volcengine_certcenter_region.placeholder\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineCertCenter, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineDCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderVolcEngineDCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.volcengine_dcdn_domain.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.volcengine_dcdn_domain.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineDCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineImageX.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderVolcEngineImageX = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.volcengine_imagex_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.volcengine_imagex_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.volcengine_imagex_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"serviceId\"]}\n        initialValue={initialValues.serviceId}\n        label={t(\"workflow_node.deploy.form.volcengine_imagex_service_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.volcengine_imagex_service_id.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.volcengine_imagex_service_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.volcengine_imagex_domain.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.volcengine_imagex_domain.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    serviceId: \"\",\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.deploy.form.volcengine_imagex_region.placeholder\")),\n    serviceId: z.string().nonempty(t(\"workflow_node.deploy.form.volcengine_imagex_service_id.placeholder\")),\n    domain: z.string().refine((v) => isDomain(v), t(\"common.errmsg.domain_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineImageX, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineLive.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst BizDeployNodeConfigFieldsProviderVolcEngineLive = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.volcengine_live_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.volcengine_live_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineLive, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineTOS.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderVolcEngineTOS = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.volcengine_tos_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.volcengine_tos_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.volcengine_tos_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"bucket\"]}\n        initialValue={initialValues.bucket}\n        label={t(\"workflow_node.deploy.form.volcengine_tos_bucket.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.volcengine_tos_bucket.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.volcengine_tos_domain.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.volcengine_tos_domain.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    bucket: \"\",\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.deploy.form.volcengine_tos_region.placeholder\")),\n    bucket: z.string().nonempty(t(\"workflow_node.deploy.form.volcengine_tos_bucket.placeholder\")),\n    domain: z.string().refine((v) => isDomain(v), t(\"common.errmsg.domain_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineTOS, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineVOD.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\nconst DOMAIN_MATCH_PATTERN_WILDCARD = \"wildcard\" as const;\nconst DOMAIN_MATCH_PATTERN_CERTSAN = \"certsan\" as const;\n\nconst DOMAIN_TYPE_PLAY = \"play\" as const;\nconst DOMAIN_TYPE_IMAGE = \"image\" as const;\n\nconst BizDeployNodeConfigFieldsProviderVolcEngineVOD = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], {\n    form: formInst,\n    preserve: true,\n  });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"spaceName\"]}\n        initialValue={initialValues.spaceName}\n        label={t(\"workflow_node.deploy.form.volcengine_vod_space_name.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.volcengine_vod_space_name.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.volcengine_vod_space_name.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainType\"]}\n        initialValue={initialValues.domainType}\n        label={t(\"workflow_node.deploy.form.volcengine_vod_domain_type.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[DOMAIN_TYPE_PLAY, DOMAIN_TYPE_IMAGE].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.volcengine_vod_domain_type.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.volcengine_vod_domain_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>\n        <Form.Item\n          name={[parentNamePath, \"domain\"]}\n          initialValue={initialValues.domain}\n          label={t(\"workflow_node.deploy.form.volcengine_vod_domain.label\")}\n          rules={[formRule]}\n        >\n          <Input placeholder={t(\"workflow_node.deploy.form.volcengine_vod_domain.placeholder\")} />\n        </Form.Item>\n      </Show>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    spaceName: \"\",\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domainType: DOMAIN_TYPE_PLAY,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      spaceName: z.string().nonempty(t(\"workflow_node.deploy.form.volcengine_vod_space_name.placeholder\")).nullish(),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domainType: z.literal([DOMAIN_TYPE_PLAY, DOMAIN_TYPE_IMAGE], t(\"workflow_node.deploy.form.volcengine_vod_domain_type.placeholder\")),\n      domain: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n          case DOMAIN_MATCH_PATTERN_WILDCARD:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineVOD, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineWAF.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst ACCESS_MODE_CNAME = \"cname\" as const;\n\nconst BizDeployNodeConfigFieldsProviderVolcEngineWAF = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"region\"]}\n        initialValue={initialValues.region}\n        label={t(\"workflow_node.deploy.form.volcengine_waf_region.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.volcengine_waf_region.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.volcengine_waf_region.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"accessMode\"]}\n        initialValue={initialValues.accessMode}\n        label={t(\"workflow_node.deploy.form.volcengine_waf_access_mode.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[ACCESS_MODE_CNAME].map((s) => ({\n            value: s,\n            label: t(`workflow_node.deploy.form.volcengine_waf_access_mode.option.${s}.label`),\n          }))}\n          placeholder={t(\"workflow_node.deploy.form.volcengine_waf_access_mode.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.volcengine_waf_domain.label\")}\n        rules={[formRule]}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.volcengine_waf_domain.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    region: \"\",\n    accessMode: ACCESS_MODE_CNAME,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    region: z.string().nonempty(t(\"workflow_node.deploy.form.volcengine_waf_region.placeholder\")),\n    accessMode: z.literal([ACCESS_MODE_CNAME], t(\"workflow_node.deploy.form.volcengine_waf_access_mode.placeholder\")),\n    domain: z.string().refine((v) => isDomain(v, { allowWildcard: true }), t(\"common.errmsg.domain_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineWAF, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderWangsuCDN.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport MultipleSplitValueInput from \"@/components/MultipleSplitValueInput\";\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst MULTIPLE_INPUT_SEPARATOR = \";\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\n\nconst BizDeployNodeConfigFieldsProviderWangsuCDN = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domains\"]}\n        initialValue={initialValues.domains}\n        label={t(\"workflow_node.deploy.form.wangsu_cdn_domains.label\")}\n        extra={t(\"workflow_node.deploy.form.wangsu_cdn_domains.help\")}\n        rules={[formRule]}\n      >\n        <MultipleSplitValueInput\n          modalTitle={t(\"workflow_node.deploy.form.wangsu_cdn_domains.multiple_input_modal.title\")}\n          placeholder={t(\"workflow_node.deploy.form.wangsu_cdn_domains.placeholder\")}\n          placeholderInModal={t(\"workflow_node.deploy.form.wangsu_cdn_domains.multiple_input_modal.placeholder\")}\n          separator={MULTIPLE_INPUT_SEPARATOR}\n          splitOptions={{ removeEmpty: true, trimSpace: true }}\n        />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domains: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domains: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n            {\n              const valid = values.domains && values.domains.split(MULTIPLE_INPUT_SEPARATOR).every((e) => isDomain(e, { allowWildcard: true }));\n              if (!valid) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domains\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderWangsuCDN, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderWangsuCDNPro.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Radio, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isDomain } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst DOMAIN_MATCH_PATTERN_EXACT = \"exact\" as const;\n\nconst BizDeployNodeConfigFieldsProviderWangsuCDNPro = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const fieldDomainMatchPattern = Form.useWatch([parentNamePath, \"domainMatchPattern\"], { form: formInst, preserve: true });\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"environment\"]}\n        initialValue={initialValues.environment}\n        label={t(\"workflow_node.deploy.form.wangsu_cdnpro_environment.label\")}\n        rules={[formRule]}\n      >\n        <Select placeholder={t(\"workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder\")}>\n          <Select.Option key=\"production\" value=\"production\">\n            {t(\"workflow_node.deploy.form.wangsu_cdnpro_environment.option.production.label\")}\n          </Select.Option>\n          <Select.Option key=\"stating\" value=\"stating\">\n            {t(\"workflow_node.deploy.form.wangsu_cdnpro_environment.option.staging.label\")}\n          </Select.Option>\n        </Select>\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domainMatchPattern\"]}\n        initialValue={initialValues.domainMatchPattern}\n        label={t(\"workflow_node.deploy.form.shared_domain_match_pattern.label\")}\n        extra={\n          fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (\n            <span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\") }}></span>\n          ) : (\n            void 0\n          )\n        }\n        rules={[formRule]}\n      >\n        <Radio.Group\n          options={[DOMAIN_MATCH_PATTERN_EXACT].map((s) => ({\n            key: s,\n            label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),\n            value: s,\n          }))}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"domain\"]}\n        initialValue={initialValues.domain}\n        label={t(\"workflow_node.deploy.form.wangsu_cdnpro_domain.label\")}\n        rules={[formRule]}\n      >\n        <Input placeholder={t(\"workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"certificateId\"]}\n        initialValue={initialValues.certificateId}\n        label={t(\"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.label\")}\n        extra={t(\"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"webhookId\"]}\n        initialValue={initialValues.webhookId}\n        label={t(\"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    environment: \"production\",\n    domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,\n    domain: \"\",\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      environment: z.literal([\"production\", \"staging\"], t(\"workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder\")),\n      domainMatchPattern: z.string().nonempty(t(\"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\")).default(DOMAIN_MATCH_PATTERN_EXACT),\n      domain: z.string().nullish(),\n      certificateId: z.string().nullish(),\n      webhookId: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.domainMatchPattern) {\n        switch (values.domainMatchPattern) {\n          case DOMAIN_MATCH_PATTERN_EXACT:\n            {\n              if (!isDomain(values.domain!, { allowWildcard: true })) {\n                ctx.addIssue({\n                  code: \"custom\",\n                  message: t(\"common.errmsg.domain_invalid\"),\n                  path: [\"domain\"],\n                });\n              }\n            }\n            break;\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderWangsuCDNPro, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderWangsuCertificate.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderWangsuCertificate = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"certificateId\"]}\n        initialValue={initialValues.certificateId}\n        label={t(\"workflow_node.deploy.form.wangsu_certificate_id.label\")}\n        extra={t(\"workflow_node.deploy.form.wangsu_certificate_id.help\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.wangsu_certificate_id.tooltip\") }}></span>}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.deploy.form.wangsu_certificate_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {};\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t: _ } = i18n;\n\n  return z.object({\n    certificateId: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderWangsuCertificate, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderWebhook.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { IconBulb } from \"@tabler/icons-react\";\nimport { Button, Form, Input, Popover } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport CodeTextInput from \"@/components/CodeTextInput\";\nimport { isJsonObject } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizDeployNodeConfigFieldsProviderWebhook = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const handleWebhookDataBlur = () => {\n    const value = formInst.getFieldValue([parentNamePath, \"webhookData\"]);\n    try {\n      const json = JSON.stringify(JSON.parse(value), null, 2);\n      formInst.setFieldValue([parentNamePath, \"webhookData\"], json);\n    } catch {\n      return;\n    }\n  };\n\n  return (\n    <>\n      <Form.Item label={t(\"workflow_node.deploy.form.webhook_data.label\")} extra={t(\"workflow_node.deploy.form.webhook_data.help\")}>\n        <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n          <Popover content={<div dangerouslySetInnerHTML={{ __html: t(\"workflow_node.deploy.form.webhook_data.vartips\") }} />} mouseEnterDelay={1}>\n            <Button color=\"default\" size=\"small\" variant=\"link\">\n              <IconBulb size=\"1.25em\" />\n            </Button>\n          </Popover>\n        </div>\n        <Form.Item name={[parentNamePath, \"webhookData\"]} initialValue={initialValues.webhookData} noStyle rules={[formRule]}>\n          <CodeTextInput\n            lineWrapping={false}\n            height=\"auto\"\n            minHeight=\"64px\"\n            maxHeight=\"256px\"\n            language=\"json\"\n            placeholder={t(\"workflow_node.deploy.form.webhook_data.placeholder\")}\n            onBlur={handleWebhookDataBlur}\n          />\n        </Form.Item>\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"timeout\"]} label={t(\"workflow_node.deploy.form.webhook_timeout.label\")} rules={[formRule]}>\n        <Input\n          type=\"number\"\n          allowClear\n          min={0}\n          max={3600}\n          placeholder={t(\"workflow_node.deploy.form.webhook_timeout.placeholder\")}\n          suffix={t(\"workflow_node.deploy.form.webhook_timeout.unit\")}\n        />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {};\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    webhookData: z\n      .string()\n      .nullish()\n      .refine((v) => {\n        if (!v) return true;\n        return isJsonObject(v);\n      }, t(\"common.errmsg.json_invalid\")),\n    timeout: z.preprocess(\n      (v) => (v == null || v === \"\" ? void 0 : Number(v)),\n      z.number().int().gte(1, t(\"workflow_node.deploy.form.webhook_timeout.placeholder\")).nullish()\n    ),\n  });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigFieldsProviderWebhook, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizDeployNodeConfigForm.tsx",
    "content": "import { useEffect, useMemo } from \"react\";\nimport { getI18n, useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconPlus } from \"@tabler/icons-react\";\nimport { type AnchorProps, Button, Divider, Form, type FormInstance, Select, Switch, Typography, theme } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport AccessEditDrawer from \"@/components/access/AccessEditDrawer\";\nimport AccessSelect from \"@/components/access/AccessSelect\";\nimport DeploymentProviderPicker from \"@/components/provider/DeploymentProviderPicker\";\nimport DeploymentProviderSelect from \"@/components/provider/DeploymentProviderSelect\";\nimport Show from \"@/components/Show\";\nimport { type AccessModel } from \"@/domain/access\";\nimport { deploymentProvidersMap } from \"@/domain/provider\";\nimport { type WorkflowNodeConfigForBizDeploy, defaultNodeConfigForBizDeploy } from \"@/domain/workflow\";\nimport { useAntdForm, useZustandShallowSelector } from \"@/hooks\";\nimport { useAccessesStore } from \"@/stores/access\";\n\nimport { getAllPreviousNodes } from \"../_util\";\nimport { FormNestedFieldsContextProvider, NodeFormContextProvider } from \"./_context\";\nimport BizDeployNodeConfigFieldsProvider from \"./BizDeployNodeConfigFieldsProvider\";\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface BizDeployNodeConfigFormProps {\n  form: FormInstance;\n  node: FlowNodeEntity;\n}\n\nconst BizDeployNodeConfigForm = ({ node, ...props }: BizDeployNodeConfigFormProps) => {\n  if (node.flowNodeType !== NodeType.BizDeploy) {\n    console.warn(`[certimate] current workflow node type is not: ${NodeType.BizDeploy}`);\n  }\n\n  const { i18n, t } = useTranslation();\n\n  const { token: themeToken } = theme.useToken();\n\n  const { accesses } = useAccessesStore(useZustandShallowSelector(\"accesses\"));\n  const accessOptionFilter = (_: string, option: AccessModel) => {\n    if (option.reserve) return false;\n    return deploymentProvidersMap.get(fieldProvider)?.provider === option.provider;\n  };\n\n  const initialValues = useMemo(() => {\n    return node.form?.getValueIn(\"config\") as WorkflowNodeConfigForBizDeploy | undefined;\n  }, [node]);\n\n  const formSchema = getSchema({ i18n }).superRefine((values, ctx) => {\n    if (values.certificateOutputNodeId) {\n      if (!certificateOutputNodeIdOptions.some((option) => option.value === values.certificateOutputNodeId)) {\n        ctx.addIssue({\n          code: \"custom\",\n          message: t(\"workflow_node.deploy.form.certificate_output_node_id.placeholder\"),\n          path: [\"certificateOutputNodeId\"],\n        });\n      }\n    }\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const { form: formInst, formProps } = useAntdForm<z.infer<typeof formSchema>>({\n    form: props.form,\n    name: \"workflowNodeBizDeployConfigForm\",\n    initialValues: initialValues ?? getInitialValues(),\n  });\n\n  const fieldProvider = Form.useWatch(\"provider\", { form: formInst, preserve: true });\n  const fieldProviderAccessId = Form.useWatch(\"providerAccessId\", { form: formInst, preserve: true });\n\n  const certificateOutputNodeIdOptions = useMemo(() => {\n    return getAllPreviousNodes(node)\n      .filter((node) => node.flowNodeType === NodeType.BizApply || node.flowNodeType === NodeType.BizUpload)\n      .map((node) => {\n        return {\n          label: node.form?.getValueIn(\"name\"),\n          value: node.id,\n        };\n      });\n  }, [node]);\n\n  const renderNestedFieldProviderComponent = BizDeployNodeConfigFieldsProvider.useComponent(fieldProvider, {});\n\n  const showProviderAccess = useMemo(() => {\n    // 内置的部署提供商（如本地部署）无需显示授权信息字段\n    if (fieldProvider) {\n      const provider = deploymentProvidersMap.get(fieldProvider);\n      return !provider?.builtin;\n    }\n\n    return false;\n  }, [fieldProvider]);\n\n  useEffect(() => {\n    // 如果未选择部署目标，则清空授权信息\n    if (!fieldProvider && fieldProviderAccessId) {\n      formInst.setFieldValue(\"providerAccessId\", void 0);\n      return;\n    }\n\n    // 如果已选择部署目标只有一个授权信息，则自动选择该授权信息\n    if (fieldProvider && !fieldProviderAccessId) {\n      const availableAccesses = accesses\n        .filter((access) => accessOptionFilter(access.provider, access))\n        .filter((access) => deploymentProvidersMap.get(fieldProvider)?.provider === access.provider);\n      if (availableAccesses.length === 1) {\n        formInst.setFieldValue(\"providerAccessId\", availableAccesses[0].id);\n      }\n    }\n  }, [fieldProvider, fieldProviderAccessId]);\n\n  const handleProviderPick = (value: string) => {\n    formInst.setFieldValue(\"provider\", value);\n    formInst.setFieldValue(\"providerAccessId\", void 0);\n    formInst.setFieldValue(\"providerConfig\", void 0);\n  };\n\n  const handleProviderSelect = (value?: string | undefined) => {\n    // 切换部署目标时重置表单，避免其他部署目标的配置字段影响当前部署目标\n    if (initialValues?.provider === value) {\n      formInst.setFieldValue(\"providerAccessId\", void 0);\n      formInst.resetFields([\"providerConfig\"]);\n    } else {\n      formInst.setFieldValue(\"providerAccessId\", void 0);\n      formInst.setFieldValue(\"providerConfig\", void 0);\n    }\n  };\n\n  return (\n    <NodeFormContextProvider value={{ node }}>\n      <Form {...formProps} clearOnDestroy={true} form={formInst} layout=\"vertical\" preserve={false} scrollToFirstError>\n        <Show when={!fieldProvider}>\n          <DeploymentProviderPicker\n            placeholder={t(\"workflow_node.deploy.form.provider.search.placeholder\")}\n            showAvailability\n            showSearch\n            onSelect={handleProviderPick}\n          />\n        </Show>\n\n        <div style={{ display: fieldProvider ? \"block\" : \"none\" }}>\n          <div id=\"parameters\" data-anchor=\"parameters\">\n            <Form.Item\n              name=\"certificateOutputNodeId\"\n              label={t(\"workflow_node.deploy.form.certificate_output_node_id.label\")}\n              extra={t(\"workflow_node.deploy.form.certificate_output_node_id.help\")}\n              rules={[formRule]}\n            >\n              <Select\n                optionRender={({ label, value }) => {\n                  return (\n                    <div className=\"flex items-center justify-between gap-4 overflow-hidden\">\n                      <div className=\"flex-1 truncate\">{label}</div>\n                      <div className=\"origin-right scale-90 font-mono text-xs\" style={{ color: themeToken.colorTextSecondary }}>\n                        (NodeID: {value})\n                      </div>\n                    </div>\n                  );\n                }}\n                options={certificateOutputNodeIdOptions}\n                placeholder={t(\"workflow_node.deploy.form.certificate_output_node_id.placeholder\")}\n              />\n            </Form.Item>\n          </div>\n\n          <div id=\"deployment\" data-anchor=\"deployment\">\n            <Divider size=\"small\">\n              <Typography.Text className=\"text-xs font-normal\" type=\"secondary\">\n                {t(\"workflow_node.deploy.form_anchor.deployment.title\")}\n              </Typography.Text>\n            </Divider>\n\n            <Form.Item name=\"provider\" label={t(\"workflow_node.deploy.form.provider.label\")} rules={[formRule]}>\n              <DeploymentProviderSelect\n                allowClear\n                disabled={!!initialValues?.provider}\n                placeholder={t(\"workflow_node.deploy.form.provider.placeholder\")}\n                showAvailability\n                showSearch\n                onSelect={handleProviderSelect}\n                onClear={handleProviderSelect}\n              />\n            </Form.Item>\n\n            <Form.Item className=\"relative\" hidden={!showProviderAccess} label={t(\"workflow_node.deploy.form.provider_access.label\")}>\n              <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n                <AccessEditDrawer\n                  data={{ provider: deploymentProvidersMap.get(fieldProvider!)?.provider }}\n                  mode=\"create\"\n                  trigger={\n                    <Button size=\"small\" type=\"link\">\n                      {t(\"workflow_node.deploy.form.provider_access.button\")}\n                      <IconPlus size=\"1.25em\" />\n                    </Button>\n                  }\n                  usage=\"hosting\"\n                  afterSubmit={(record) => {\n                    if (!accessOptionFilter(record.provider, record)) return;\n                    if (deploymentProvidersMap.get(fieldProvider!)?.provider !== record.provider) return;\n                    formInst.setFieldValue(\"providerAccessId\", record.id);\n                  }}\n                />\n              </div>\n              <Form.Item name=\"providerAccessId\" dependencies={[\"provider\"]} rules={[formRule]} noStyle>\n                <AccessSelect\n                  disabled={!fieldProvider}\n                  placeholder={t(\"workflow_node.deploy.form.provider_access.placeholder\")}\n                  showSearch\n                  onFilter={accessOptionFilter}\n                />\n              </Form.Item>\n            </Form.Item>\n\n            <FormNestedFieldsContextProvider value={{ parentNamePath: \"providerConfig\" }}>\n              {renderNestedFieldProviderComponent && <>{renderNestedFieldProviderComponent}</>}\n            </FormNestedFieldsContextProvider>\n          </div>\n\n          <div id=\"strategy\" data-anchor=\"strategy\">\n            <Divider size=\"small\">\n              <Typography.Text className=\"text-xs font-normal\" type=\"secondary\">\n                {t(\"workflow_node.deploy.form_anchor.strategy.title\")}\n              </Typography.Text>\n            </Divider>\n\n            <Form.Item label={t(\"workflow_node.deploy.form.skip_on_last_succeeded.label\")}>\n              <span className=\"me-2 inline-block\">{t(\"workflow_node.deploy.form.skip_on_last_succeeded.prefix\")}</span>\n              <span className=\"inline-block\">\n                <Form.Item name=\"skipOnLastSucceeded\" noStyle rules={[formRule]}>\n                  <Switch\n                    checkedChildren={t(\"workflow_node.deploy.form.skip_on_last_succeeded.switch.on\")}\n                    unCheckedChildren={t(\"workflow_node.deploy.form.skip_on_last_succeeded.switch.off\")}\n                  />\n                </Form.Item>\n              </span>\n              <span className=\"ms-2 inline-block\">{t(\"workflow_node.deploy.form.skip_on_last_succeeded.suffix\")}</span>\n            </Form.Item>\n          </div>\n        </div>\n      </Form>\n    </NodeFormContextProvider>\n  );\n};\n\nconst getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }): Required<AnchorProps>[\"items\"] => {\n  const { t } = i18n;\n\n  return [\"parameters\", \"deployment\", \"strategy\"].map((key) => ({\n    key: key,\n    title: t(`workflow_node.deploy.form_anchor.${key}.tab`),\n    href: \"#\" + key,\n  }));\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    ...(defaultNodeConfigForBizDeploy() as Nullish<z.infer<ReturnType<typeof getSchema>>>),\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      certificateOutputNodeId: z.string().nonempty(t(\"workflow_node.deploy.form.certificate_output_node_id.placeholder\")),\n      provider: z.string().nonempty(t(\"workflow_node.deploy.form.provider.placeholder\")),\n      providerAccessId: z.string().nullish(),\n      providerConfig: z.any().nullish(),\n      skipOnLastSucceeded: z.boolean().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.provider) {\n        const provider = deploymentProvidersMap.get(values.provider);\n        if (!provider?.builtin && !values.providerAccessId) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: t(\"workflow_node.deploy.form.provider_access.placeholder\"),\n            path: [\"providerAccessId\"],\n          });\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(BizDeployNodeConfigForm, {\n  getAnchorItems,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizMonitorNodeConfigDrawer.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { Form } from \"antd\";\n\nimport { NodeConfigDrawer } from \"./_shared\";\nimport BizMonitorNodeConfigForm from \"./BizMonitorNodeConfigForm\";\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface BizMonitorNodeConfigDrawerProps {\n  afterClose?: () => void;\n  loading?: boolean;\n  node: FlowNodeEntity;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst BizMonitorNodeConfigDrawer = ({ node, ...props }: BizMonitorNodeConfigDrawerProps) => {\n  if (node.flowNodeType !== NodeType.BizMonitor) {\n    console.warn(`[certimate] current workflow node type is not: ${NodeType.BizMonitor}`);\n  }\n\n  const { i18n } = useTranslation();\n\n  const [formInst] = Form.useForm();\n\n  return (\n    <NodeConfigDrawer\n      anchor={{\n        items: BizMonitorNodeConfigForm.getAnchorItems({ i18n }),\n      }}\n      form={formInst}\n      node={node}\n      {...props}\n    >\n      <BizMonitorNodeConfigForm form={formInst} node={node} />\n    </NodeConfigDrawer>\n  );\n};\n\nexport default BizMonitorNodeConfigDrawer;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizMonitorNodeConfigForm.tsx",
    "content": "import { useMemo } from \"react\";\nimport { getI18n, useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { type AnchorProps, Form, type FormInstance, Input, InputNumber } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport Tips from \"@/components/Tips\";\nimport { type WorkflowNodeConfigForBizMonitor, defaultNodeConfigForBizMonitor } from \"@/domain/workflow\";\nimport { useAntdForm } from \"@/hooks\";\nimport { isDomain, isHostname, isPortNumber } from \"@/utils/validator\";\n\nimport { NodeFormContextProvider } from \"./_context\";\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface BizMonitorNodeConfigFormProps {\n  form: FormInstance;\n  node: FlowNodeEntity;\n}\n\nconst BizMonitorNodeConfigForm = ({ node, ...props }: BizMonitorNodeConfigFormProps) => {\n  if (node.flowNodeType !== NodeType.BizMonitor) {\n    console.warn(`[certimate] current workflow node type is not: ${NodeType.BizMonitor}`);\n  }\n\n  const { i18n, t } = useTranslation();\n\n  const initialValues = useMemo(() => {\n    return node.form?.getValueIn(\"config\") as WorkflowNodeConfigForBizMonitor | undefined;\n  }, [node]);\n\n  const formSchema = getSchema({ i18n });\n  const formRule = createSchemaFieldRule(formSchema);\n  const { form: formInst, formProps } = useAntdForm<z.infer<typeof formSchema>>({\n    form: props.form,\n    name: \"workflowNodeBizMonitorConfigForm\",\n    initialValues: initialValues ?? getInitialValues(),\n  });\n\n  return (\n    <NodeFormContextProvider value={{ node }}>\n      <Form {...formProps} clearOnDestroy={true} form={formInst} layout=\"vertical\" preserve={false} scrollToFirstError>\n        <div id=\"parameters\" data-anchor=\"parameters\">\n          <Form.Item>\n            <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.monitor.form.guide\") }}></span>} />\n          </Form.Item>\n\n          <div className=\"flex space-x-2\">\n            <div className=\"w-2/3\">\n              <Form.Item name=\"host\" label={t(\"workflow_node.monitor.form.host.label\")} rules={[formRule]}>\n                <Input placeholder={t(\"workflow_node.monitor.form.host.placeholder\")} />\n              </Form.Item>\n            </div>\n\n            <div className=\"w-1/3\">\n              <Form.Item name=\"port\" label={t(\"workflow_node.monitor.form.port.label\")} rules={[formRule]}>\n                <InputNumber style={{ width: \"100%\" }} min={1} max={65535} placeholder={t(\"workflow_node.monitor.form.port.placeholder\")} />\n              </Form.Item>\n            </div>\n          </div>\n\n          <Form.Item name=\"domain\" label={t(\"workflow_node.monitor.form.domain.label\")} extra={t(\"workflow_node.monitor.form.domain.help\")} rules={[formRule]}>\n            <Input placeholder={t(\"workflow_node.monitor.form.domain.placeholder\")} />\n          </Form.Item>\n\n          <Form.Item name=\"requestPath\" label={t(\"workflow_node.monitor.form.request_path.label\")} rules={[formRule]}>\n            <Input placeholder={t(\"workflow_node.monitor.form.request_path.placeholder\")} />\n          </Form.Item>\n        </div>\n      </Form>\n    </NodeFormContextProvider>\n  );\n};\n\nconst getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }): Required<AnchorProps>[\"items\"] => {\n  const { t } = i18n;\n\n  return [\"parameters\"].map((key) => ({\n    key: key,\n    title: t(`workflow_node.monitor.form_anchor.${key}.tab`),\n    href: \"#\" + key,\n  }));\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return defaultNodeConfigForBizMonitor();\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    host: z.string().refine((v) => isHostname(v), t(\"common.errmsg.host_invalid\")),\n    port: z.coerce.number().refine((v) => isPortNumber(v), t(\"common.errmsg.port_invalid\")),\n    domain: z\n      .string()\n      .nullish()\n      .refine((v) => {\n        if (!v) return true;\n        return isDomain(v);\n      }, t(\"common.errmsg.domain_invalid\")),\n    requestPath: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(BizMonitorNodeConfigForm, {\n  getAnchorItems,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizNotifyNodeConfigDrawer.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { Form } from \"antd\";\n\nimport { type WorkflowNodeConfigForBizNotify } from \"@/domain/workflow\";\n\nimport { NodeConfigDrawer } from \"./_shared\";\nimport BizNotifyNodeConfigForm from \"./BizNotifyNodeConfigForm\";\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface BizNotifyNodeConfigDrawerProps {\n  afterClose?: () => void;\n  loading?: boolean;\n  node: FlowNodeEntity;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst BizNotifyNodeConfigDrawer = ({ node, ...props }: BizNotifyNodeConfigDrawerProps) => {\n  if (node.flowNodeType !== NodeType.BizNotify) {\n    console.warn(`[certimate] current workflow node type is not: ${NodeType.BizNotify}`);\n  }\n\n  const { i18n } = useTranslation();\n\n  const [formInst] = Form.useForm();\n\n  const fieldProvider = Form.useWatch<WorkflowNodeConfigForBizNotify[\"provider\"]>(\"provider\", { form: formInst, preserve: true });\n\n  return (\n    <NodeConfigDrawer\n      anchor={fieldProvider ? { items: BizNotifyNodeConfigForm.getAnchorItems({ i18n }) } : false}\n      footer={fieldProvider ? void 0 : false}\n      form={formInst}\n      node={node}\n      {...props}\n    >\n      <BizNotifyNodeConfigForm form={formInst} node={node} />\n    </NodeConfigDrawer>\n  );\n};\n\nexport default BizNotifyNodeConfigDrawer;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProvider.tsx",
    "content": "﻿import { useEffect, useState } from \"react\";\n\nimport { NOTIFICATION_PROVIDERS, type NotificationProviderType } from \"@/domain/provider\";\n\nimport BizNotifyNodeConfigFieldsProviderDiscordBot from \"./BizNotifyNodeConfigFieldsProviderDiscordBot\";\nimport BizNotifyNodeConfigFieldsProviderEmail from \"./BizNotifyNodeConfigFieldsProviderEmail\";\nimport BizNotifyNodeConfigFieldsProviderMattermost from \"./BizNotifyNodeConfigFieldsProviderMattermost\";\nimport BizNotifyNodeConfigFieldsProviderSlackBot from \"./BizNotifyNodeConfigFieldsProviderSlackBot\";\nimport BizNotifyNodeConfigFieldsProviderTelegramBot from \"./BizNotifyNodeConfigFieldsProviderTelegramBot\";\nimport BizNotifyNodeConfigFieldsProviderWebhook from \"./BizNotifyNodeConfigFieldsProviderWebhook\";\n\nconst providerComponentMap: Partial<Record<NotificationProviderType, React.ComponentType<any>>> = {\n  /*\n    注意：如果追加新的子组件，请保持以 ASCII 排序。\n    NOTICE: If you add new child component, please keep ASCII order.\n    */\n  [NOTIFICATION_PROVIDERS.DISCORDBOT]: BizNotifyNodeConfigFieldsProviderDiscordBot,\n  [NOTIFICATION_PROVIDERS.EMAIL]: BizNotifyNodeConfigFieldsProviderEmail,\n  [NOTIFICATION_PROVIDERS.MATTERMOST]: BizNotifyNodeConfigFieldsProviderMattermost,\n  [NOTIFICATION_PROVIDERS.SLACKBOT]: BizNotifyNodeConfigFieldsProviderSlackBot,\n  [NOTIFICATION_PROVIDERS.TELEGRAMBOT]: BizNotifyNodeConfigFieldsProviderTelegramBot,\n  [NOTIFICATION_PROVIDERS.WEBHOOK]: BizNotifyNodeConfigFieldsProviderWebhook,\n};\n\nconst useComponent = (provider: string, { initProps, deps = [] }: { initProps?: (provider: string) => any; deps?: unknown[] }) => {\n  const initComponent = () => {\n    const Component = providerComponentMap[provider as NotificationProviderType];\n    if (!Component) return null;\n\n    const props = initProps?.(provider);\n    if (props) {\n      return <Component {...props} />;\n    }\n\n    return <Component />;\n  };\n\n  const [component, setComponent] = useState(() => initComponent());\n\n  useEffect(() => setComponent(initComponent()), [provider]);\n  useEffect(() => setComponent(initComponent()), deps);\n\n  return component;\n};\n\nconst _default = {\n  useComponent,\n};\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProviderDiscordBot.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizNotifyNodeConfigFieldsProviderDiscordBot = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"channelId\"]}\n        initialValue={initialValues.channelId}\n        label={t(\"workflow_node.notify.form.discordbot_channel_id.label\")}\n        extra={t(\"workflow_node.notify.form.discordbot_channel_id.help\")}\n        rules={[formRule]}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.notify.form.discordbot_channel_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {};\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t: _ } = i18n;\n\n  return z.object({\n    channelId: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(BizNotifyNodeConfigFieldsProviderDiscordBot, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProviderEmail.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input, Select } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { isEmail } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst MESSAGE_FORMAT_PLAIN = \"plain\" as const;\nconst MESSAGE_FORMAT_HTML = \"html\" as const;\n\nconst BizNotifyNodeConfigFieldsProviderEmail = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"format\"]}\n        initialValue={initialValues.format}\n        label={t(\"workflow_node.notify.form.email_format.label\")}\n        rules={[formRule]}\n      >\n        <Select\n          options={[MESSAGE_FORMAT_PLAIN, MESSAGE_FORMAT_HTML].map((s) => ({\n            key: s,\n            label: t(`workflow_node.notify.form.email_format.option.${s}.label`),\n            value: s,\n          }))}\n          placeholder={t(\"workflow_node.notify.form.email_format.placeholder\")}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name={[parentNamePath, \"receiverAddress\"]}\n        initialValue={initialValues.receiverAddress}\n        label={t(\"workflow_node.notify.form.email_receiver_address.label\")}\n        extra={t(\"workflow_node.notify.form.email_receiver_address.help\")}\n        rules={[formRule]}\n      >\n        <Input type=\"email\" allowClear placeholder={t(\"workflow_node.notify.form.email_receiver_address.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    format: MESSAGE_FORMAT_PLAIN,\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    format: z.enum([MESSAGE_FORMAT_PLAIN, MESSAGE_FORMAT_HTML]).nullish(),\n    receiverAddress: z\n      .string()\n      .nullish()\n      .refine((v) => {\n        if (!v) return true;\n        return isEmail(v);\n      }, t(\"common.errmsg.email_invalid\")),\n  });\n};\n\nconst _default = Object.assign(BizNotifyNodeConfigFieldsProviderEmail, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProviderMattermost.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizNotifyNodeConfigFieldsProviderMattermost = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"channelId\"]}\n        initialValue={initialValues.channelId}\n        label={t(\"workflow_node.notify.form.mattermost_channel_id.label\")}\n        extra={t(\"workflow_node.notify.form.mattermost_channel_id.help\")}\n        rules={[formRule]}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.notify.form.mattermost_channel_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {};\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t: _ } = i18n;\n\n  return z.object({\n    channelId: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(BizNotifyNodeConfigFieldsProviderMattermost, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProviderSlackBot.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizNotifyNodeConfigFieldsProviderSlackBot = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"channelId\"]}\n        initialValue={initialValues.channelId}\n        label={t(\"workflow_node.notify.form.slackbot_channel_id.label\")}\n        extra={t(\"workflow_node.notify.form.slackbot_channel_id.help\")}\n        rules={[formRule]}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.notify.form.slackbot_channel_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {};\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t: _ } = i18n;\n\n  return z.object({\n    channelId: z.string().nullish(),\n  });\n};\n\nconst _default = Object.assign(BizNotifyNodeConfigFieldsProviderSlackBot, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProviderTelegramBot.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { Form, Input } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizNotifyNodeConfigFieldsProviderTelegramBot = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const initialValues = getInitialValues();\n\n  return (\n    <>\n      <Form.Item\n        name={[parentNamePath, \"chatId\"]}\n        initialValue={initialValues.chatId}\n        label={t(\"workflow_node.notify.form.telegrambot_chat_id.label\")}\n        extra={t(\"workflow_node.notify.form.telegrambot_chat_id.help\")}\n        rules={[formRule]}\n      >\n        <Input allowClear placeholder={t(\"workflow_node.notify.form.telegrambot_chat_id.placeholder\")} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {};\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    chatId: z\n      .preprocess(\n        (v) => (v == null || v === \"\" ? void 0 : Number(v)),\n        z\n          .number()\n          .nullish()\n          .refine((v) => {\n            if (v == null || v + \"\" === \"\") return true;\n            return !Number.isNaN(+v!) && +v! !== 0;\n          }, t(\"workflow_node.notify.form.telegrambot_chat_id.placeholder\"))\n      )\n      .nullish(),\n  });\n};\n\nconst _default = Object.assign(BizNotifyNodeConfigFieldsProviderTelegramBot, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProviderWebhook.tsx",
    "content": "import { getI18n, useTranslation } from \"react-i18next\";\nimport { IconBulb } from \"@tabler/icons-react\";\nimport { Button, Form, Input, Popover } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport CodeTextInput from \"@/components/CodeTextInput\";\nimport { isJsonObject } from \"@/utils/validator\";\n\nimport { useFormNestedFieldsContext } from \"./_context\";\n\nconst BizNotifyNodeConfigFieldsProviderWebhook = () => {\n  const { i18n, t } = useTranslation();\n\n  const { parentNamePath } = useFormNestedFieldsContext();\n  const formSchema = z.object({\n    [parentNamePath]: getSchema({ i18n }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const formInst = Form.useFormInstance();\n  const initialValues = getInitialValues();\n\n  const handleWebhookDataBlur = () => {\n    const value = formInst.getFieldValue([parentNamePath, \"webhookData\"]);\n    try {\n      const json = JSON.stringify(JSON.parse(value), null, 2);\n      formInst.setFieldValue([parentNamePath, \"webhookData\"], json);\n    } catch {\n      return;\n    }\n  };\n\n  return (\n    <>\n      <Form.Item label={t(\"workflow_node.notify.form.webhook_data.label\")} extra={t(\"workflow_node.notify.form.webhook_data.help\")}>\n        <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n          <Popover content={<div dangerouslySetInnerHTML={{ __html: t(\"workflow_node.notify.form.webhook_data.vartips\") }} />} mouseEnterDelay={1}>\n            <Button color=\"default\" size=\"small\" variant=\"link\">\n              <IconBulb size=\"1.25em\" />\n            </Button>\n          </Popover>\n        </div>\n        <Form.Item name={[parentNamePath, \"webhookData\"]} initialValue={initialValues.webhookData} noStyle rules={[formRule]}>\n          <CodeTextInput\n            lineWrapping={false}\n            height=\"auto\"\n            minHeight=\"64px\"\n            maxHeight=\"256px\"\n            language=\"json\"\n            placeholder={t(\"workflow_node.notify.form.webhook_data.placeholder\")}\n            onBlur={handleWebhookDataBlur}\n          />\n        </Form.Item>\n      </Form.Item>\n\n      <Form.Item name={[parentNamePath, \"timeout\"]} label={t(\"workflow_node.notify.form.webhook_timeout.label\")} rules={[formRule]}>\n        <Input\n          type=\"number\"\n          allowClear\n          min={0}\n          max={3600}\n          placeholder={t(\"workflow_node.notify.form.webhook_timeout.placeholder\")}\n          suffix={t(\"workflow_node.notify.form.webhook_timeout.unit\")}\n        />\n      </Form.Item>\n    </>\n  );\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {};\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    webhookData: z\n      .string()\n      .nullish()\n      .refine((v) => {\n        if (!v) return true;\n        return isJsonObject(v);\n      }, t(\"common.errmsg.json_invalid\")),\n    timeout: z.preprocess(\n      (v) => (v == null || v === \"\" ? void 0 : Number(v)),\n      z.number().int().gte(1, t(\"workflow_node.notify.form.webhook_timeout.placeholder\")).nullish()\n    ),\n  });\n};\n\nconst _default = Object.assign(BizNotifyNodeConfigFieldsProviderWebhook, {\n  getInitialValues,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizNotifyNodeConfigForm.tsx",
    "content": "﻿import { useEffect, useMemo } from \"react\";\nimport { getI18n, useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconChevronDown, IconPlus } from \"@tabler/icons-react\";\nimport { type AnchorProps, Button, Divider, Form, type FormInstance, Input, Switch, Typography } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport AccessEditDrawer from \"@/components/access/AccessEditDrawer\";\nimport AccessSelect from \"@/components/access/AccessSelect\";\nimport PresetNotifyTemplatesPopselect from \"@/components/preset/PresetNotifyTemplatesPopselect\";\nimport NotificationProviderPicker from \"@/components/provider/NotificationProviderPicker\";\nimport NotificationProviderSelect from \"@/components/provider/NotificationProviderSelect\";\nimport Show from \"@/components/Show\";\nimport Tips from \"@/components/Tips\";\nimport { type AccessModel } from \"@/domain/access\";\nimport { notificationProvidersMap } from \"@/domain/provider\";\nimport { type WorkflowNodeConfigForBizNotify, defaultNodeConfigForBizNotify } from \"@/domain/workflow\";\nimport { useAntdForm, useZustandShallowSelector } from \"@/hooks\";\nimport { useAccessesStore } from \"@/stores/access\";\n\nimport { FormNestedFieldsContextProvider, NodeFormContextProvider } from \"./_context\";\nimport BizNotifyNodeConfigFieldsProvider from \"./BizNotifyNodeConfigFieldsProvider\";\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface BizNotifyNodeConfigFormProps {\n  form: FormInstance;\n  node: FlowNodeEntity;\n}\n\nconst BizNotifyNodeConfigForm = ({ node, ...props }: BizNotifyNodeConfigFormProps) => {\n  if (node.flowNodeType !== NodeType.BizNotify) {\n    console.warn(`[certimate] current workflow node type is not: ${NodeType.BizNotify}`);\n  }\n\n  const { i18n, t } = useTranslation();\n\n  const { accesses } = useAccessesStore(useZustandShallowSelector(\"accesses\"));\n  const accessOptionFilter = (_: string, option: AccessModel) => {\n    if (option.reserve !== \"notif\") return false;\n    return notificationProvidersMap.get(fieldProvider)?.provider === option.provider;\n  };\n\n  const initialValues = useMemo(() => {\n    return node.form?.getValueIn(\"config\") as WorkflowNodeConfigForBizNotify | undefined;\n  }, [node]);\n\n  const formSchema = getSchema({ i18n });\n  const formRule = createSchemaFieldRule(formSchema);\n  const { form: formInst, formProps } = useAntdForm<z.infer<typeof formSchema>>({\n    form: props.form,\n    name: \"workflowNodeBizNotifyNodeConfigForm\",\n    initialValues: initialValues ?? getInitialValues(),\n  });\n\n  const fieldProvider = Form.useWatch(\"provider\", { form: formInst, preserve: true });\n  const fieldProviderAccessId = Form.useWatch(\"providerAccessId\", { form: formInst, preserve: true });\n\n  const renderNestedFieldProviderComponent = BizNotifyNodeConfigFieldsProvider.useComponent(fieldProvider, {});\n\n  useEffect(() => {\n    // 如果未选择通知渠道，则清空授权信息\n    if (!fieldProvider && fieldProviderAccessId) {\n      formInst.setFieldValue(\"providerAccessId\", void 0);\n      return;\n    }\n\n    // 如果已选择通知渠道只有一个授权信息，则自动选择该授权信息\n    if (fieldProvider && !fieldProviderAccessId) {\n      const availableAccesses = accesses\n        .filter((access) => accessOptionFilter(access.provider, access))\n        .filter((access) => notificationProvidersMap.get(fieldProvider)?.provider === access.provider);\n      if (availableAccesses.length === 1) {\n        formInst.setFieldValue(\"providerAccessId\", availableAccesses[0].id);\n      }\n    }\n  }, [fieldProvider, fieldProviderAccessId]);\n\n  const handleProviderPick = (value: string) => {\n    formInst.setFieldValue(\"provider\", value);\n    formInst.setFieldValue(\"providerAccessId\", void 0);\n    formInst.setFieldValue(\"providerConfig\", void 0);\n  };\n\n  const handleProviderSelect = (value?: string | undefined) => {\n    // 切换通知渠道时重置表单，避免其他通知渠道的配置字段影响当前通知渠道\n    if (initialValues?.provider === value) {\n      formInst.setFieldValue(\"providerAccessId\", void 0);\n      formInst.resetFields([\"providerConfig\"]);\n    } else {\n      formInst.setFieldValue(\"providerAccessId\", void 0);\n      formInst.setFieldValue(\"providerConfig\", void 0);\n    }\n  };\n\n  return (\n    <NodeFormContextProvider value={{ node }}>\n      <Form {...formProps} clearOnDestroy={true} form={formInst} layout=\"vertical\" preserve={false} scrollToFirstError>\n        <Show when={!fieldProvider}>\n          <NotificationProviderPicker\n            placeholder={t(\"workflow_node.notify.form.provider.search.placeholder\")}\n            showAvailability\n            showSearch\n            onSelect={handleProviderPick}\n          />\n        </Show>\n\n        <div style={{ display: fieldProvider ? \"block\" : \"none\" }}>\n          <div id=\"parameters\" data-anchor=\"parameters\">\n            <Form.Item name=\"subject\" label={t(\"workflow_node.notify.form.subject.label\")} rules={[formRule]}>\n              <Input placeholder={t(\"workflow_node.notify.form.subject.placeholder\")} />\n            </Form.Item>\n\n            <Form.Item label={t(\"workflow_node.notify.form.message.label\")}>\n              <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n                <PresetNotifyTemplatesPopselect\n                  trigger={[\"click\"]}\n                  onSelect={(_, template) => {\n                    if (template) {\n                      formInst.setFieldValue(\"subject\", template.subject);\n                      formInst.setFieldValue(\"message\", template.message);\n                    }\n                  }}\n                >\n                  <Button size=\"small\" type=\"link\">\n                    {t(\"preset.dropdown.notification.button\")}\n                    <IconChevronDown size=\"1.25em\" />\n                  </Button>\n                </PresetNotifyTemplatesPopselect>\n              </div>\n              <Form.Item name=\"message\" noStyle rules={[formRule]}>\n                <Input.TextArea autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t(\"workflow_node.notify.form.message.placeholder\")} />\n              </Form.Item>\n            </Form.Item>\n\n            <Form.Item>\n              <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.notify.form.template.guide\") }}></span>} />\n            </Form.Item>\n          </div>\n\n          <div id=\"channel\" data-anchor=\"channel\">\n            <Divider size=\"small\">\n              <Typography.Text className=\"text-xs font-normal\" type=\"secondary\">\n                {t(\"workflow_node.notify.form_anchor.channel.title\")}\n              </Typography.Text>\n            </Divider>\n\n            <Form.Item name=\"provider\" label={t(\"workflow_node.notify.form.provider.label\")} rules={[formRule]}>\n              <NotificationProviderSelect\n                allowClear\n                disabled={!!initialValues?.provider}\n                placeholder={t(\"workflow_node.notify.form.provider.placeholder\")}\n                showAvailability\n                showSearch\n                onSelect={handleProviderSelect}\n                onClear={handleProviderSelect}\n              />\n            </Form.Item>\n\n            <Form.Item label={t(\"workflow_node.notify.form.provider_access.label\")}>\n              <div className=\"absolute -top-1.5 right-0 -translate-y-full\">\n                <AccessEditDrawer\n                  data={{ provider: notificationProvidersMap.get(fieldProvider!)?.provider }}\n                  mode=\"create\"\n                  trigger={\n                    <Button size=\"small\" type=\"link\">\n                      {t(\"workflow_node.notify.form.provider_access.button\")}\n                      <IconPlus size=\"1.25em\" />\n                    </Button>\n                  }\n                  usage=\"notification\"\n                  afterSubmit={(record) => {\n                    if (!accessOptionFilter(record.provider, record)) return;\n                    if (notificationProvidersMap.get(fieldProvider!)?.provider !== record.provider) return;\n                    formInst.setFieldValue(\"providerAccessId\", record.id);\n                  }}\n                />\n              </div>\n              <Form.Item name=\"providerAccessId\" dependencies={[\"provider\"]} noStyle rules={[formRule]}>\n                <AccessSelect\n                  disabled={!fieldProvider}\n                  placeholder={t(\"workflow_node.notify.form.provider_access.placeholder\")}\n                  showSearch\n                  onFilter={accessOptionFilter}\n                />\n              </Form.Item>\n            </Form.Item>\n\n            <FormNestedFieldsContextProvider value={{ parentNamePath: \"providerConfig\" }}>\n              {renderNestedFieldProviderComponent && <>{renderNestedFieldProviderComponent}</>}\n            </FormNestedFieldsContextProvider>\n          </div>\n\n          <div id=\"strategy\" data-anchor=\"strategy\">\n            <Divider size=\"small\">\n              <Typography.Text className=\"text-xs font-normal\" type=\"secondary\">\n                {t(\"workflow_node.notify.form_anchor.strategy.title\")}\n              </Typography.Text>\n            </Divider>\n\n            <Form.Item label={t(\"workflow_node.notify.form.skip_on_all_prev_skipped.label\")}>\n              <span className=\"me-2 inline-block\">{t(\"workflow_node.notify.form.skip_on_all_prev_skipped.prefix\")}</span>\n              <span className=\"inline-block\">\n                <Form.Item name=\"skipOnAllPrevSkipped\" noStyle rules={[formRule]}>\n                  <Switch\n                    checkedChildren={t(\"workflow_node.notify.form.skip_on_all_prev_skipped.switch.on\")}\n                    unCheckedChildren={t(\"workflow_node.notify.form.skip_on_all_prev_skipped.switch.off\")}\n                  />\n                </Form.Item>\n              </span>\n              <span className=\"ms-2 inline-block\">{t(\"workflow_node.notify.form.skip_on_all_prev_skipped.suffix\")}</span>\n            </Form.Item>\n          </div>\n        </div>\n      </Form>\n    </NodeFormContextProvider>\n  );\n};\n\nconst getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }): Required<AnchorProps>[\"items\"] => {\n  const { t } = i18n;\n\n  return [\"parameters\", \"channel\", \"strategy\"].map((key) => ({\n    key: key,\n    title: t(`workflow_node.notify.form_anchor.${key}.tab`),\n    href: \"#\" + key,\n  }));\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    subject: \"\",\n    message: \"\",\n    ...(defaultNodeConfigForBizNotify() as Nullish<z.infer<ReturnType<typeof getSchema>>>),\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z.object({\n    subject: z.string().nonempty(t(\"workflow_node.notify.form.subject.placeholder\")),\n    message: z.string().nonempty(t(\"workflow_node.notify.form.message.placeholder\")),\n    provider: z.string().nonempty(t(\"workflow_node.notify.form.provider.placeholder\")),\n    providerAccessId: z.string().nonempty(t(\"workflow_node.notify.form.provider_access.placeholder\")),\n    providerConfig: z.any().nullish(),\n    skipOnAllPrevSkipped: z.boolean().nullish(),\n  });\n};\n\nconst _default = Object.assign(BizNotifyNodeConfigForm, {\n  getAnchorItems,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizUploadNodeConfigDrawer.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { Form } from \"antd\";\n\nimport { NodeConfigDrawer } from \"./_shared\";\nimport BizUploadNodeConfigForm from \"./BizUploadNodeConfigForm\";\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface BizUploadNodeConfigDrawerProps {\n  afterClose?: () => void;\n  loading?: boolean;\n  node: FlowNodeEntity;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst BizUploadNodeConfigDrawer = ({ node, ...props }: BizUploadNodeConfigDrawerProps) => {\n  if (node.flowNodeType !== NodeType.BizUpload) {\n    console.warn(`[certimate] current workflow node type is not: ${NodeType.BizUpload}`);\n  }\n\n  const { i18n } = useTranslation();\n\n  const [formInst] = Form.useForm();\n\n  return (\n    <NodeConfigDrawer\n      anchor={{\n        items: BizUploadNodeConfigForm.getAnchorItems({ i18n }),\n      }}\n      form={formInst}\n      node={node}\n      {...props}\n    >\n      <BizUploadNodeConfigForm form={formInst} node={node} />\n    </NodeConfigDrawer>\n  );\n};\n\nexport default BizUploadNodeConfigDrawer;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BizUploadNodeConfigForm.tsx",
    "content": "import { useMemo } from \"react\";\nimport { getI18n, useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { type AnchorProps, Form, type FormInstance, Input, Radio } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport FileTextInput from \"@/components/FileTextInput\";\nimport Show from \"@/components/Show\";\nimport Tips from \"@/components/Tips\";\nimport { type WorkflowNodeConfigForBizUpload, defaultNodeConfigForBizUpload } from \"@/domain/workflow\";\nimport { useAntdForm } from \"@/hooks\";\nimport { isUrlWithHttpOrHttps } from \"@/utils/validator\";\nimport { getCertificateSubjectAltNames as getX509SubjectAltNames, validatePEMCertificate, validatePEMPrivateKey } from \"@/utils/x509\";\n\nimport { NodeFormContextProvider } from \"./_context\";\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface BizUploadNodeConfigFormProps {\n  form: FormInstance;\n  node: FlowNodeEntity;\n}\n\nconst UPLOAD_SOURCE_FORM = \"form\" as const;\nconst UPLOAD_SOURCE_LOCAL = \"local\" as const;\nconst UPLOAD_SOURCE_URL = \"url\" as const;\n\nconst BizUploadNodeConfigForm = ({ node, ...props }: BizUploadNodeConfigFormProps) => {\n  if (node.flowNodeType !== NodeType.BizUpload) {\n    console.warn(`[certimate] current workflow node type is not: ${NodeType.BizUpload}`);\n  }\n\n  const { i18n, t } = useTranslation();\n\n  const initialValues = useMemo(() => {\n    return node.form?.getValueIn(\"config\") as WorkflowNodeConfigForBizUpload | undefined;\n  }, [node]);\n\n  const formSchema = getSchema({ i18n });\n  const formRule = createSchemaFieldRule(formSchema);\n  const { form: formInst, formProps } = useAntdForm<z.infer<typeof formSchema>>({\n    form: props.form,\n    name: \"workflowNodeBizUploadConfigForm\",\n    initialValues: initialValues ?? getInitialValues(),\n  });\n\n  const fieldSource = Form.useWatch(\"source\", { form: formInst, preserve: true });\n  const fieldCertificate = Form.useWatch(\"certificate\", { form: formInst, preserve: true });\n  const fieldName = useMemo(() => {\n    if (!fieldSource || fieldSource === UPLOAD_SOURCE_FORM) {\n      return fieldCertificate ? getX509SubjectAltNames(fieldCertificate).join(\";\") : void 0;\n    }\n    return void 0;\n  }, [fieldSource, fieldCertificate]);\n\n  const handleSourceChange = (value: string) => {\n    if (value === initialValues?.source) {\n      formInst.resetFields([\"certificate\", \"privateKey\"]);\n    } else {\n      setTimeout(() => {\n        formInst.setFieldValue(\"certificate\", \"\");\n        formInst.setFieldValue(\"privateKey\", \"\");\n      }, 0);\n    }\n  };\n\n  return (\n    <NodeFormContextProvider value={{ node }}>\n      <Form {...formProps} clearOnDestroy={true} form={formInst} layout=\"vertical\" preserve={false} scrollToFirstError>\n        <div id=\"parameters\" data-anchor=\"parameters\">\n          <Form.Item name=\"source\" label={t(\"workflow_node.upload.form.source.label\")} rules={[formRule]}>\n            <Radio.Group block onChange={(e) => handleSourceChange(e.target.value)}>\n              <Radio.Button value={UPLOAD_SOURCE_FORM}>{t(\"workflow_node.upload.form.source.option.form.label\")}</Radio.Button>\n              <Radio.Button value={UPLOAD_SOURCE_LOCAL}>{t(\"workflow_node.upload.form.source.option.local.label\")}</Radio.Button>\n              <Radio.Button value={UPLOAD_SOURCE_URL}>{t(\"workflow_node.upload.form.source.option.url.label\")}</Radio.Button>\n            </Radio.Group>\n          </Form.Item>\n\n          <Show when={fieldSource === UPLOAD_SOURCE_FORM}>\n            <Form.Item label={t(\"workflow_node.upload.form.name.label\")}>\n              <Input placeholder={t(\"workflow_node.upload.form.name.placeholder\")} readOnly value={fieldName} variant=\"filled\" />\n            </Form.Item>\n\n            <Form.Item name=\"certificate\" label={t(\"workflow_node.upload.form.certificate_pem.label\")} rules={[formRule]}>\n              <FileTextInput autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t(\"workflow_node.upload.form.certificate_pem.placeholder\")} />\n            </Form.Item>\n\n            <Form.Item name=\"privateKey\" label={t(\"workflow_node.upload.form.private_key_pem.label\")} rules={[formRule]}>\n              <FileTextInput autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t(\"workflow_node.upload.form.private_key_pem.placeholder\")} />\n            </Form.Item>\n          </Show>\n\n          <Show when={fieldSource === UPLOAD_SOURCE_LOCAL}>\n            <Form.Item>\n              <Tips message={t(\"workflow_node.upload.form.guide\")} />\n            </Form.Item>\n\n            <Form.Item name=\"certificate\" label={t(\"workflow_node.upload.form.certificate_path.label\")} rules={[formRule]}>\n              <Input placeholder={t(\"workflow_node.upload.form.certificate_path.placeholder\")} />\n            </Form.Item>\n\n            <Form.Item name=\"privateKey\" label={t(\"workflow_node.upload.form.private_key_path.label\")} rules={[formRule]}>\n              <Input placeholder={t(\"workflow_node.upload.form.private_key_path.placeholder\")} />\n            </Form.Item>\n          </Show>\n\n          <Show when={fieldSource === UPLOAD_SOURCE_URL}>\n            <Form.Item>\n              <Tips message={t(\"workflow_node.upload.form.guide\")} />\n            </Form.Item>\n\n            <Form.Item name=\"certificate\" label={t(\"workflow_node.upload.form.certificate_url.label\")} rules={[formRule]}>\n              <Input placeholder={t(\"workflow_node.upload.form.certificate_url.placeholder\")} />\n            </Form.Item>\n\n            <Form.Item name=\"privateKey\" label={t(\"workflow_node.upload.form.private_key_url.label\")} rules={[formRule]}>\n              <Input placeholder={t(\"workflow_node.upload.form.private_key_url.placeholder\")} />\n            </Form.Item>\n          </Show>\n        </div>\n      </Form>\n    </NodeFormContextProvider>\n  );\n};\n\nconst getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }): Required<AnchorProps>[\"items\"] => {\n  const { t } = i18n;\n\n  return [\"parameters\"].map((key) => ({\n    key: key,\n    title: t(`workflow_node.upload.form_anchor.${key}.tab`),\n    href: \"#\" + key,\n  }));\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    certificate: \"\",\n    privateKey: \"\",\n    ...(defaultNodeConfigForBizUpload() as Nullish<z.infer<ReturnType<typeof getSchema>>>),\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      source: z.enum([UPLOAD_SOURCE_FORM, UPLOAD_SOURCE_LOCAL, UPLOAD_SOURCE_URL], t(\"workflow_node.upload.form.source.placeholder\")),\n      name: z.string().nullish(),\n      certificate: z.string().nonempty(),\n      privateKey: z.string().nonempty(),\n    })\n    .superRefine((values, ctx) => {\n      switch (values.source) {\n        case UPLOAD_SOURCE_FORM:\n          {\n            if (!validatePEMCertificate(values.certificate)) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.upload.form.certificate_pem.errmsg.invalid\"),\n                path: [\"certificate\"],\n              });\n            }\n\n            if (!validatePEMPrivateKey(values.privateKey)) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.upload.form.private_key_pem.errmsg.invalid\"),\n                path: [\"privateKey\"],\n              });\n            }\n          }\n          break;\n\n        case UPLOAD_SOURCE_LOCAL:\n          {\n            if (!z.string().nonempty().safeParse(values.certificate).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.upload.form.certificate_path.placeholder\"),\n                path: [\"certificate\"],\n              });\n            }\n\n            if (!z.string().nonempty().safeParse(values.privateKey).success) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.upload.form.private_key_path.placeholder\"),\n                path: [\"privateKey\"],\n              });\n            }\n          }\n          break;\n\n        case UPLOAD_SOURCE_URL:\n          {\n            if (!isUrlWithHttpOrHttps(values.certificate)) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.upload.form.certificate_url.placeholder\"),\n                path: [\"certificate\"],\n              });\n            }\n\n            if (!isUrlWithHttpOrHttps(values.privateKey)) {\n              ctx.addIssue({\n                code: \"custom\",\n                message: t(\"workflow_node.upload.form.private_key_url.placeholder\"),\n                path: [\"privateKey\"],\n              });\n            }\n          }\n          break;\n\n        default:\n          {\n            ctx.addIssue({\n              code: \"custom\",\n              message: t(\"workflow_node.upload.form.source.placeholder\"),\n              path: [\"source\"],\n            });\n          }\n          break;\n      }\n    });\n};\n\nconst _default = Object.assign(BizUploadNodeConfigForm, {\n  getAnchorItems,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BranchBlockNodeConfigDrawer.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { Form } from \"antd\";\n\nimport { NodeConfigDrawer } from \"./_shared\";\nimport BranchBlockNodeConfigForm from \"./BranchBlockNodeConfigForm\";\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface BranchBlockNodeConfigDrawerProps {\n  afterClose?: () => void;\n  loading?: boolean;\n  node: FlowNodeEntity;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst BranchBlockNodeConfigDrawer = ({ node, ...props }: BranchBlockNodeConfigDrawerProps) => {\n  if (node.flowNodeType !== NodeType.BranchBlock) {\n    console.warn(`[certimate] current workflow node type is not: ${NodeType.BranchBlock}`);\n  }\n\n  const { i18n } = useTranslation();\n\n  const [formInst] = Form.useForm();\n\n  return (\n    <NodeConfigDrawer\n      anchor={{\n        items: BranchBlockNodeConfigForm.getAnchorItems({ i18n }),\n      }}\n      form={formInst}\n      node={node}\n      {...props}\n    >\n      <BranchBlockNodeConfigForm form={formInst} node={node} />\n    </NodeConfigDrawer>\n  );\n};\n\nexport default BranchBlockNodeConfigDrawer;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BranchBlockNodeConfigExprInputBox.tsx",
    "content": "import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { IconCircleMinus, IconCirclePlus } from \"@tabler/icons-react\";\nimport { useControllableValue } from \"ahooks\";\nimport { Button, Form, Input, Radio, Select, theme } from \"antd\";\n\nimport Show from \"@/components/Show\";\nimport {\n  type Expr,\n  type ExprComparisonOperator,\n  type ExprLogicalOperator,\n  ExprType,\n  type ExprValue,\n  type ExprValueSelector,\n  type ExprValueType,\n} from \"@/domain/workflow\";\nimport { useAntdFormName } from \"@/hooks\";\n\nimport { useNodeFormContext } from \"./_context\";\nimport { getAllPreviousNodes } from \"../_util\";\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface BranchBlockNodeConfigExprInputBoxProps {\n  className?: string;\n  style?: React.CSSProperties;\n  defaultValue?: Expr;\n  value?: Expr;\n  onChange?: (value: Expr) => void;\n}\n\nexport interface BranchBlockNodeConfigExprInputBoxInstance {\n  validate: () => Promise<Expr | undefined>;\n}\n\n// 表单内部使用的扁平结构\ntype ConditionItem = {\n  // 选择器，格式为 \"${nodeId}#${outputName}#${valueType}\"\n  // 将 [ExprValueSelector] 转为字符串形式，以便于结构化存储。\n  leftSelector?: string;\n  // 比较运算符。\n  operator?: ExprComparisonOperator;\n  // 值。\n  // 将 [ExprValue] 转为字符串形式，以便于结构化存储。\n  rightValue?: string;\n};\n\ntype ConditionFormValues = {\n  conditions: ConditionItem[];\n  logicalOperator: ExprLogicalOperator;\n};\n\nconst exprToFormValues = (expr: Expr | undefined): ConditionFormValues => {\n  if (!expr) return getInitialValues();\n\n  const conditions: ConditionItem[] = [];\n  let logicalOp: ExprLogicalOperator = \"and\";\n\n  const extractExpr = (expr: Expr): void => {\n    if (expr.type === ExprType.Comparison) {\n      if (expr.left.type == ExprType.Variant && expr.right.type == ExprType.Constant) {\n        conditions.push({\n          leftSelector: expr.left.selector?.id != null ? `${expr.left.selector.id}#${expr.left.selector.name}#${expr.left.selector.type}` : void 0,\n          operator: expr.operator != null ? expr.operator : void 0,\n          rightValue: expr.right?.value != null ? String(expr.right.value) : void 0,\n        });\n      } else {\n        console.warn(\"[certimate] invalid comparison expression: left must be a variant and right must be a constant\", expr);\n      }\n    } else if (expr.type === ExprType.Logical) {\n      logicalOp = expr.operator || \"and\";\n      extractExpr(expr.left);\n      extractExpr(expr.right);\n    }\n  };\n\n  extractExpr(expr);\n\n  return {\n    conditions: conditions,\n    logicalOperator: logicalOp,\n  };\n};\n\nconst formValuesToExpr = (values: ConditionFormValues): Expr | undefined => {\n  const wrapExpr = (condition: ConditionItem): Expr => {\n    const [id, name, type] = (condition.leftSelector?.split(\"#\") ?? [\"\", \"\", \"\"]) as [string, string, ExprValueType];\n    const valid = !!id && !!name && !!type;\n\n    const left: Expr = {\n      type: ExprType.Variant,\n      selector: valid\n        ? {\n            id: id,\n            name: name,\n            type: type,\n          }\n        : ({} as ExprValueSelector),\n    };\n\n    const right: Expr = {\n      type: ExprType.Constant,\n      value: condition.rightValue!,\n      valueType: type,\n    };\n\n    return {\n      type: ExprType.Comparison,\n      operator: condition.operator!,\n      left,\n      right,\n    };\n  };\n\n  if (values.conditions.length === 0) {\n    return;\n  }\n\n  // 只有一个条件时，直接返回比较表达式\n  if (values.conditions.length === 1) {\n    const { leftSelector, operator, rightValue } = values.conditions[0];\n    if (!leftSelector || !operator || !rightValue) {\n      return;\n    }\n    return wrapExpr(values.conditions[0]);\n  }\n\n  // 多个条件时，通过逻辑运算符连接\n  let expr: Expr = wrapExpr(values.conditions[0]);\n  for (let i = 1; i < values.conditions.length; i++) {\n    expr = {\n      type: ExprType.Logical,\n      operator: values.logicalOperator,\n      left: expr,\n      right: wrapExpr(values.conditions[i]),\n    };\n  }\n  return expr;\n};\n\nconst BranchBlockNodeConfigExprInputBox = forwardRef<BranchBlockNodeConfigExprInputBoxInstance, BranchBlockNodeConfigExprInputBoxProps>(\n  ({ className, style, ...props }, ref) => {\n    const { t } = useTranslation();\n\n    const { token: themeToken } = theme.useToken();\n\n    const [value, setValue] = useControllableValue<Expr | undefined>(props, {\n      valuePropName: \"value\",\n      defaultValuePropName: \"defaultValue\",\n      trigger: \"onChange\",\n    });\n\n    const { node } = useNodeFormContext();\n\n    const [formInst] = Form.useForm<ConditionFormValues>();\n    const formName = useAntdFormName({ form: formInst, name: \"workflowNodeBranchBlockConfigExprInputBoxForm\" });\n    const [formModel, setFormModel] = useState<ConditionFormValues>(getInitialValues());\n\n    useEffect(() => {\n      if (value) {\n        const formValues = exprToFormValues(value);\n        formInst.setFieldsValue(formValues);\n        setFormModel(formValues);\n      } else {\n        formInst.resetFields();\n        setFormModel(getInitialValues());\n      }\n    }, [value]);\n\n    const ciSelectorOptions = useMemo(() => {\n      return getAllPreviousNodes(node)\n        .filter((node) => node.flowNodeType === NodeType.BizApply || node.flowNodeType === NodeType.BizUpload || node.flowNodeType === NodeType.BizMonitor)\n        .map((node) => {\n          const form = node.form;\n          const group = {\n            data: {\n              name: form?.getValueIn(\"name\"),\n              ...form?.values,\n            },\n            label: (\n              <div className=\"flex items-center justify-between gap-4 overflow-hidden\">\n                <div className=\"flex-1 truncate\">{form?.getValueIn(\"name\")}</div>\n                <div className=\"origin-right scale-90 font-mono text-xs\" style={{ color: themeToken.colorTextSecondary }}>\n                  (NodeID: {node.id})\n                </div>\n              </div>\n            ),\n            options: Array<{ label: string; value: string }>(),\n          };\n\n          group.options.push({\n            label: `${t(\"workflow.variables.type.certificate.label\")} - ${t(\"workflow.variables.selector.hours_left.label\")}`,\n            value: `${node.id}#certificate.hoursLeft#number`,\n          });\n          group.options.push({\n            label: `${t(\"workflow.variables.type.certificate.label\")} - ${t(\"workflow.variables.selector.days_left.label\")}`,\n            value: `${node.id}#certificate.daysLeft#number`,\n          });\n          group.options.push({\n            label: `${t(\"workflow.variables.type.certificate.label\")} - ${t(\"workflow.variables.selector.validity.label\")}`,\n            value: `${node.id}#certificate.validity#boolean`,\n          });\n\n          return group;\n        })\n        .filter((item) => item.options.length > 0);\n    }, [node]);\n\n    const getValueTypeBySelector = (selector: string): ExprValueType | undefined => {\n      if (!selector) return;\n\n      const parts = selector.split(\"#\");\n      if (parts.length >= 3) {\n        return parts[2].toLowerCase() as ExprValueType;\n      }\n    };\n\n    const getOperatorsBySelector = (selector: string): { value: ExprComparisonOperator; label: string }[] => {\n      const valueType = getValueTypeBySelector(selector);\n      return getOperatorsByValueType(valueType!);\n    };\n\n    const getOperatorsByValueType = (valueType: ExprValue): { value: ExprComparisonOperator; label: string }[] => {\n      switch (valueType) {\n        case \"number\":\n          return [\n            { value: \"eq\", label: t(\"workflow_node.branch_block.form.expression.operator.option.eq.label\") },\n            { value: \"neq\", label: t(\"workflow_node.branch_block.form.expression.operator.option.neq.label\") },\n            { value: \"gt\", label: t(\"workflow_node.branch_block.form.expression.operator.option.gt.label\") },\n            { value: \"gte\", label: t(\"workflow_node.branch_block.form.expression.operator.option.gte.label\") },\n            { value: \"lt\", label: t(\"workflow_node.branch_block.form.expression.operator.option.lt.label\") },\n            { value: \"lte\", label: t(\"workflow_node.branch_block.form.expression.operator.option.lte.label\") },\n          ];\n\n        case \"string\":\n          return [\n            { value: \"eq\", label: t(\"workflow_node.branch_block.form.expression.operator.option.eq.label\") },\n            { value: \"neq\", label: t(\"workflow_node.branch_block.form.expression.operator.option.neq.label\") },\n          ];\n\n        case \"boolean\":\n          return [\n            { value: \"eq\", label: t(\"workflow_node.branch_block.form.expression.operator.option.eq.alias_is_label\") },\n            { value: \"neq\", label: t(\"workflow_node.branch_block.form.expression.operator.option.neq.alias_not_label\") },\n          ];\n\n        default:\n          return [];\n      }\n    };\n\n    const handleFormChange = (_: unknown, values: ConditionFormValues) => {\n      // TODO: 这里直接用参数 `values` 会丢失部分字段，引发 Issue #1096。\n      // 暂时先用 `getFieldsValue()` 代替，待排查原因，疑似与 antd v6 升级有关。\n      setTimeout(() => {\n        values = formInst.getFieldsValue();\n        const expr = formValuesToExpr(values);\n        setValue(expr);\n      }, 0);\n    };\n\n    useImperativeHandle(ref, () => {\n      return {\n        validate: async () => {\n          const formValues = await formInst.validateFields();\n          return formValuesToExpr(formValues);\n        },\n      };\n    });\n\n    return (\n      <Form className={className} style={style} form={formInst} initialValues={formModel} layout=\"vertical\" name={formName} onValuesChange={handleFormChange}>\n        <Show when={formModel.conditions?.length > 1}>\n          <Form.Item name=\"logicalOperator\" rules={[{ required: true, message: t(\"workflow_node.branch_block.form.expression.logical_operator.errmsg\") }]}>\n            <Radio.Group block>\n              <Radio.Button value=\"and\">{t(\"workflow_node.branch_block.form.expression.logical_operator.option.and.label\")}</Radio.Button>\n              <Radio.Button value=\"or\">{t(\"workflow_node.branch_block.form.expression.logical_operator.option.or.label\")}</Radio.Button>\n            </Radio.Group>\n          </Form.Item>\n        </Show>\n\n        <Form.List name=\"conditions\">\n          {(fields, { add, remove }) => (\n            <div className=\"flex flex-col gap-2\">\n              {fields.map(({ key, name: index, ...rest }) => (\n                <div key={key} className=\"flex gap-2\">\n                  {/* 左：变量选择器 */}\n                  <Form.Item\n                    className=\"mb-0 flex-1\"\n                    name={[index, \"leftSelector\"]}\n                    rules={[{ required: true, message: t(\"workflow_node.branch_block.form.expression.variable.errmsg\") }]}\n                    {...rest}\n                  >\n                    <Select\n                      labelRender={({ label, value }) => {\n                        if (value != null) {\n                          const group = ciSelectorOptions.find((group) => group.options.some((option) => option.value === value));\n                          return `${group?.data?.name} - ${label}`;\n                        }\n\n                        return (\n                          <span style={{ color: themeToken.colorTextPlaceholder }}>{t(\"workflow_node.branch_block.form.expression.variable.placeholder\")}</span>\n                        );\n                      }}\n                      options={ciSelectorOptions}\n                      placeholder={t(\"workflow_node.branch_block.form.expression.variable.placeholder\")}\n                    />\n                  </Form.Item>\n\n                  {/* 中：运算符选择器，根据变量类型决定选项 */}\n                  <Form.Item\n                    noStyle\n                    shouldUpdate={(prevValues, currentValues) => {\n                      return prevValues.conditions?.[index]?.leftSelector !== currentValues.conditions?.[index]?.leftSelector;\n                    }}\n                  >\n                    {({ getFieldValue }) => {\n                      const leftSelector = getFieldValue([\"conditions\", index, \"leftSelector\"]);\n                      const operators = getOperatorsBySelector(leftSelector);\n\n                      return (\n                        <Form.Item\n                          className=\"mb-0 w-36\"\n                          name={[index, \"operator\"]}\n                          rules={[{ required: true, message: t(\"workflow_node.branch_block.form.expression.operator.errmsg\") }]}\n                          {...rest}\n                        >\n                          <Select\n                            open={operators.length === 0 ? false : void 0}\n                            options={operators}\n                            placeholder={t(\"workflow_node.branch_block.form.expression.operator.placeholder\")}\n                          />\n                        </Form.Item>\n                      );\n                    }}\n                  </Form.Item>\n\n                  {/* 右：输入控件，根据变量类型决定组件 */}\n                  <Form.Item\n                    noStyle\n                    shouldUpdate={(prevValues, currentValues) => {\n                      return prevValues.conditions?.[index]?.leftSelector !== currentValues.conditions?.[index]?.leftSelector;\n                    }}\n                  >\n                    {({ getFieldValue }) => {\n                      const leftSelector = getFieldValue([\"conditions\", index, \"leftSelector\"]);\n                      const valueType = getValueTypeBySelector(leftSelector);\n\n                      return (\n                        <Form.Item\n                          className=\"mb-0 w-36\"\n                          name={[index, \"rightValue\"]}\n                          rules={[{ required: true, message: t(\"workflow_node.branch_block.form.expression.value.errmsg\") }]}\n                          {...rest}\n                        >\n                          {valueType === \"string\" ? (\n                            <Input placeholder={t(\"workflow_node.branch_block.form.expression.value.placeholder\")} />\n                          ) : valueType === \"number\" ? (\n                            <Input type=\"number\" placeholder={t(\"workflow_node.branch_block.form.expression.value.placeholder\")} />\n                          ) : valueType === \"boolean\" ? (\n                            <Select placeholder={t(\"workflow_node.branch_block.form.expression.value.placeholder\")}>\n                              <Select.Option value=\"true\">{t(\"workflow_node.branch_block.form.expression.value.option.true.label\")}</Select.Option>\n                              <Select.Option value=\"false\">{t(\"workflow_node.branch_block.form.expression.value.option.false.label\")}</Select.Option>\n                            </Select>\n                          ) : (\n                            <Input readOnly placeholder={t(\"workflow_node.branch_block.form.expression.value.placeholder\")} />\n                          )}\n                        </Form.Item>\n                      );\n                    }}\n                  </Form.Item>\n\n                  <Button color=\"default\" icon={<IconCircleMinus size=\"1.25em\" />} type=\"text\" onClick={() => remove(index)} />\n                </div>\n              ))}\n\n              <Form.Item noStyle>\n                <Button type=\"dashed\" block icon={<IconCirclePlus size=\"1.25em\" />} onClick={() => add({})}>\n                  {t(\"workflow_node.branch_block.form.expression.add_condition.button\")}\n                </Button>\n              </Form.Item>\n            </div>\n          )}\n        </Form.List>\n      </Form>\n    );\n  }\n);\n\nconst getInitialValues = (): ConditionFormValues => {\n  return {\n    conditions: [{}],\n    logicalOperator: \"and\",\n  };\n};\n\nexport default BranchBlockNodeConfigExprInputBox;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/BranchBlockNodeConfigForm.tsx",
    "content": "import { useMemo, useRef } from \"react\";\nimport { getI18n, useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { type AnchorProps, Form, type FormInstance } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport {\n  type Expr,\n  type ExprComparisonOperator,\n  type ExprLogicalOperator,\n  ExprType,\n  type ExprValueType,\n  type WorkflowNodeConfigForBranchBlock,\n  defaultNodeConfigForBranchBlock,\n} from \"@/domain/workflow\";\n\nimport { useAntdForm } from \"@/hooks\";\n\nimport { NodeFormContextProvider } from \"./_context\";\nimport BranchBlockNodeConfigExprInputBox, { type BranchBlockNodeConfigExprInputBoxInstance } from \"./BranchBlockNodeConfigExprInputBox\";\n\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface BranchBlockNodeConfigFormProps {\n  form: FormInstance;\n  node: FlowNodeEntity;\n}\n\nconst BranchBlockNodeConfigForm = ({ node, ...props }: BranchBlockNodeConfigFormProps) => {\n  if (node.flowNodeType !== NodeType.BranchBlock) {\n    console.warn(`[certimate] current workflow node type is not: ${NodeType.BranchBlock}`);\n  }\n\n  const { i18n, t } = useTranslation();\n\n  const initialValues = useMemo(() => {\n    return node.form?.getValueIn(\"config\") as WorkflowNodeConfigForBranchBlock | undefined;\n  }, [node]);\n\n  const formSchema = getSchema({ i18n }).superRefine(async (values, ctx) => {\n    if (values.expression != null) {\n      try {\n        await exprInputBoxRef.current!.validate();\n      } catch {\n        if (!ctx.issues.some((issue) => issue.path?.[0] === \"expression\")) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: t(\"workflow_node.branch_block.form.expression.errmsg.invalid\"),\n            path: [\"expression\"],\n          });\n        }\n      }\n    }\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const { form: formInst, formProps } = useAntdForm<z.infer<typeof formSchema>>({\n    form: props.form,\n    name: \"workflowNodeBranchBlockConfigForm\",\n    initialValues: initialValues ?? getInitialValues(),\n  });\n\n  const exprInputBoxRef = useRef<BranchBlockNodeConfigExprInputBoxInstance>(null);\n\n  return (\n    <NodeFormContextProvider value={{ node }}>\n      <Form {...formProps} clearOnDestroy={true} form={formInst} layout=\"vertical\" preserve={false} scrollToFirstError>\n        <div id=\"parameters\" data-anchor=\"parameters\">\n          <Form.Item name=\"expression\" label={t(\"workflow_node.branch_block.form.expression.label\")} rules={[formRule]} validateTrigger={false}>\n            <BranchBlockNodeConfigExprInputBox ref={exprInputBoxRef} />\n          </Form.Item>\n        </div>\n      </Form>\n    </NodeFormContextProvider>\n  );\n};\n\nconst getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }): Required<AnchorProps>[\"items\"] => {\n  const { t } = i18n;\n\n  return [\"parameters\"].map((key) => ({\n    key: key,\n    title: t(`workflow_node.branch_block.form_anchor.${key}.tab`),\n    href: \"#\" + key,\n  }));\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return defaultNodeConfigForBranchBlock();\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  const exprSchema: z.ZodType<Expr> = z.lazy(() =>\n    z.discriminatedUnion(\"type\", [\n      z.object({\n        type: z.literal(ExprType.Constant),\n        value: z.string(),\n        valueType: z.string<ExprValueType>(),\n      }),\n\n      z.object({\n        type: z.literal(ExprType.Variant),\n        selector: z.object({\n          id: z.string(),\n          name: z.string(),\n          type: z.string<ExprValueType>(),\n        }),\n      }),\n\n      z.object({\n        type: z.literal(ExprType.Comparison),\n        operator: z.string<ExprComparisonOperator>(),\n        left: exprSchema,\n        right: exprSchema,\n      }),\n\n      z.object({\n        type: z.literal(ExprType.Logical),\n        operator: z.string<ExprLogicalOperator>(),\n        left: exprSchema,\n        right: exprSchema,\n      }),\n\n      z.object({\n        type: z.literal(ExprType.Not),\n        expr: exprSchema,\n      }),\n    ])\n  );\n\n  return z.object({\n    expression: z\n      .any()\n      .nullish()\n      .refine((v) => v == null || exprSchema.safeParse(v).success, t(\"workflow_node.branch_block.form.expression.errmsg.invalid\")),\n  });\n};\n\nconst _default = Object.assign(BranchBlockNodeConfigForm, {\n  getAnchorItems,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/DelayNodeConfigDrawer.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { Form } from \"antd\";\n\nimport { NodeConfigDrawer } from \"./_shared\";\nimport DelayNodeConfigForm from \"./DelayNodeConfigForm\";\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface DelayNodeConfigDrawerProps {\n  afterClose?: () => void;\n  loading?: boolean;\n  node: FlowNodeEntity;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst DelayNodeConfigDrawer = ({ node, ...props }: DelayNodeConfigDrawerProps) => {\n  if (node.flowNodeType !== NodeType.Delay) {\n    console.warn(`[certimate] current workflow node type is not: ${NodeType.Delay}`);\n  }\n\n  const { i18n } = useTranslation();\n\n  const [formInst] = Form.useForm();\n\n  return (\n    <NodeConfigDrawer\n      anchor={{\n        items: DelayNodeConfigForm.getAnchorItems({ i18n }),\n      }}\n      form={formInst}\n      node={node}\n      {...props}\n    >\n      <DelayNodeConfigForm form={formInst} node={node} />\n    </NodeConfigDrawer>\n  );\n};\n\nexport default DelayNodeConfigDrawer;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/DelayNodeConfigForm.tsx",
    "content": "﻿import { useMemo } from \"react\";\nimport { getI18n, useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { type AnchorProps, Form, type FormInstance, InputNumber } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { type WorkflowNodeConfigForDelay, defaultNodeConfigForDelay } from \"@/domain/workflow\";\nimport { useAntdForm } from \"@/hooks\";\n\nimport { NodeFormContextProvider } from \"./_context\";\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface DelayNodeConfigFormProps {\n  form: FormInstance;\n  node: FlowNodeEntity;\n}\n\nconst DelayNodeConfigForm = ({ node, ...props }: DelayNodeConfigFormProps) => {\n  if (node.flowNodeType !== NodeType.Delay) {\n    console.warn(`[certimate] current workflow node type is not: ${NodeType.Delay}`);\n  }\n\n  const { i18n, t } = useTranslation();\n\n  const initialValues = useMemo(() => {\n    return node.form?.getValueIn(\"config\") as WorkflowNodeConfigForDelay | undefined;\n  }, [node]);\n\n  const formSchema = getSchema({ i18n });\n  const formRule = createSchemaFieldRule(formSchema);\n  const { form: formInst, formProps } = useAntdForm<z.infer<typeof formSchema>>({\n    form: props.form,\n    name: \"workflowNodeDelayConfigForm\",\n    initialValues: initialValues ?? getInitialValues(),\n  });\n\n  return (\n    <NodeFormContextProvider value={{ node }}>\n      <Form {...formProps} clearOnDestroy={true} form={formInst} layout=\"vertical\" preserve={false} scrollToFirstError>\n        <div id=\"parameters\" data-anchor=\"parameters\">\n          <Form.Item name=\"wait\" label={t(\"workflow_node.delay.form.wait.label\")} rules={[formRule]}>\n            <InputNumber\n              style={{ width: \"100%\" }}\n              min={0}\n              max={3600}\n              placeholder={t(\"workflow_node.delay.form.wait.placeholder\")}\n              suffix={t(\"workflow_node.delay.form.wait.unit\")}\n            />\n          </Form.Item>\n        </div>\n      </Form>\n    </NodeFormContextProvider>\n  );\n};\n\nconst getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }): Required<AnchorProps>[\"items\"] => {\n  const { t } = i18n;\n\n  return [\"parameters\"].map((key) => ({\n    key: key,\n    title: t(`workflow_node.delay.form_anchor.${key}.tab`),\n    href: \"#\" + key,\n  }));\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    ...(defaultNodeConfigForDelay() as Nullish<z.infer<ReturnType<typeof getSchema>>>),\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t: _ } = i18n;\n\n  return z.object({\n    wait: z.coerce.number().int().positive(),\n  });\n};\n\nconst _default = Object.assign(DelayNodeConfigForm, {\n  getAnchorItems,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/StartNodeConfigDrawer.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { Form } from \"antd\";\n\nimport { NodeConfigDrawer } from \"./_shared\";\nimport StartNodeConfigForm from \"./StartNodeConfigForm\";\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface StartNodeConfigDrawerProps {\n  afterClose?: () => void;\n  loading?: boolean;\n  node: FlowNodeEntity;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst StartNodeConfigDrawer = ({ node, ...props }: StartNodeConfigDrawerProps) => {\n  if (node.flowNodeType !== NodeType.Start) {\n    console.warn(`[certimate] current workflow node type is not: ${NodeType.Start}`);\n  }\n\n  const { i18n } = useTranslation();\n\n  const [formInst] = Form.useForm();\n\n  return (\n    <NodeConfigDrawer\n      anchor={{\n        items: StartNodeConfigForm.getAnchorItems({ i18n }),\n      }}\n      form={formInst}\n      node={node}\n      {...props}\n    >\n      <StartNodeConfigForm form={formInst} node={node} />\n    </NodeConfigDrawer>\n  );\n};\n\nexport default StartNodeConfigDrawer;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/StartNodeConfigForm.tsx",
    "content": "﻿import { useEffect, useMemo, useState } from \"react\";\nimport { getI18n, useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconDice6 } from \"@tabler/icons-react\";\nimport { type AnchorProps, Button, Form, type FormInstance, Input, Radio, Space } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport dayjs from \"dayjs\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport Tips from \"@/components/Tips\";\nimport { WORKFLOW_TRIGGERS, type WorkflowNodeConfigForStart, defaultNodeConfigForStart } from \"@/domain/workflow\";\nimport { useAntdForm } from \"@/hooks\";\nimport { getNextCronExecutions, validateCronExpression } from \"@/utils/cron\";\n\nimport { NodeFormContextProvider } from \"./_context\";\nimport { NodeType } from \"../nodes/typings\";\n\nexport interface StartNodeConfigFormProps {\n  form: FormInstance;\n  node: FlowNodeEntity;\n}\n\nconst StartNodeConfigForm = ({ node, ...props }: StartNodeConfigFormProps) => {\n  if (node.flowNodeType !== NodeType.Start) {\n    console.warn(`[certimate] current workflow node type is not: ${NodeType.Start}`);\n  }\n\n  const { i18n, t } = useTranslation();\n\n  const initialValues = useMemo(() => {\n    return node.form?.getValueIn(\"config\") as WorkflowNodeConfigForStart | undefined;\n  }, [node]);\n\n  const formSchema = getSchema({ i18n });\n  const formRule = createSchemaFieldRule(formSchema);\n  const { form: formInst, formProps } = useAntdForm<z.infer<typeof formSchema>>({\n    form: props.form,\n    name: \"workflowNodeStartConfigForm\",\n    initialValues: initialValues ?? getInitialValues(),\n  });\n\n  const fieldTrigger = Form.useWatch(\"trigger\", formInst);\n  const fieldTriggerCron = Form.useWatch(\"triggerCron\", formInst);\n  const [fieldTriggerCronExpectedExecutions, setFieldTriggerCronExpectedExecutions] = useState<Date[]>([]);\n  useEffect(() => {\n    setFieldTriggerCronExpectedExecutions(getNextCronExecutions(fieldTriggerCron!, 5));\n  }, [fieldTriggerCron]);\n\n  const handleTriggerChange = (value: string) => {\n    if (value === WORKFLOW_TRIGGERS.SCHEDULED) {\n      formInst.setFieldValue(\"triggerCron\", initialValues?.triggerCron || \"0 0 * * *\");\n    } else {\n      formInst.setFieldValue(\"triggerCron\", void 0);\n    }\n  };\n\n  const handleRandomCronClick = () => {\n    const m = Math.floor(Math.random() * 60);\n    const h = Math.floor(Math.random() * 24);\n    formInst.setFieldValue(\"triggerCron\", `${m} ${h} * * *`);\n  };\n\n  return (\n    <NodeFormContextProvider value={{ node }}>\n      <Form {...formProps} clearOnDestroy={true} form={formInst} layout=\"vertical\" preserve={false} scrollToFirstError>\n        <div id=\"parameters\" data-anchor=\"parameters\">\n          <Form.Item name=\"trigger\" label={t(\"workflow_node.start.form.trigger.label\")} rules={[formRule]}>\n            <Radio.Group onChange={(e) => handleTriggerChange(e.target.value)}>\n              <Radio value={WORKFLOW_TRIGGERS.MANUAL}>{t(\"workflow_node.start.form.trigger.option.manual.label\")}</Radio>\n              <Radio value={WORKFLOW_TRIGGERS.SCHEDULED}>{t(\"workflow_node.start.form.trigger.option.scheduled.label\")}</Radio>\n            </Radio.Group>\n          </Form.Item>\n\n          <Form.Item\n            hidden={fieldTrigger !== WORKFLOW_TRIGGERS.SCHEDULED}\n            label={t(\"workflow_node.start.form.trigger_cron.label\")}\n            tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.start.form.trigger_cron.tooltip\") }}></span>}\n            extra={\n              <Show when={fieldTriggerCronExpectedExecutions.length > 0}>\n                <div>\n                  {t(\"workflow_node.start.form.trigger_cron.help\")}\n                  <br />\n                  {fieldTriggerCronExpectedExecutions.map((date, index) => (\n                    <span key={index}>\n                      {dayjs(date).format(\"YYYY-MM-DD HH:mm:ss\")}\n                      <br />\n                    </span>\n                  ))}\n                </div>\n              </Show>\n            }\n          >\n            <Space.Compact className=\"w-full\">\n              <Form.Item name=\"triggerCron\" noStyle rules={[formRule]}>\n                <Input placeholder={t(\"workflow_node.start.form.trigger_cron.placeholder\")} />\n              </Form.Item>\n              <Button className=\"px-2\" onClick={handleRandomCronClick}>\n                <IconDice6 size=\"1.25em\" />\n              </Button>\n            </Space.Compact>\n          </Form.Item>\n\n          <Show when={fieldTrigger === WORKFLOW_TRIGGERS.SCHEDULED}>\n            <Form.Item>\n              <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.start.form.trigger_cron.guide\") }}></span>} />\n            </Form.Item>\n          </Show>\n        </div>\n      </Form>\n    </NodeFormContextProvider>\n  );\n};\n\nconst getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }): Required<AnchorProps>[\"items\"] => {\n  const { t } = i18n;\n\n  return [\"parameters\"].map((key) => ({\n    key: key,\n    title: t(`workflow_node.start.form_anchor.${key}.tab`),\n    href: \"#\" + key,\n  }));\n};\n\nconst getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {\n  return {\n    trigger: WORKFLOW_TRIGGERS.MANUAL,\n    ...(defaultNodeConfigForStart() as Nullish<z.infer<ReturnType<typeof getSchema>>>),\n  };\n};\n\nconst getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {\n  const { t } = i18n;\n\n  return z\n    .object({\n      trigger: z.string().nonempty(t(\"workflow_node.start.form.trigger.placeholder\")),\n      triggerCron: z.string().nullish(),\n    })\n    .superRefine((values, ctx) => {\n      if (values.trigger === WORKFLOW_TRIGGERS.SCHEDULED) {\n        if (!validateCronExpression(values.triggerCron!)) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: t(\"workflow_node.start.form.trigger_cron.errmsg.invalid\"),\n            path: [\"triggerCron\"],\n          });\n        }\n      }\n    });\n};\n\nconst _default = Object.assign(StartNodeConfigForm, {\n  getAnchorItems,\n  getSchema,\n});\n\nexport default _default;\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/_context.ts",
    "content": "﻿import { createContext, useContext } from \"react\";\nimport { type FlowNodeEntity } from \"@flowgram.ai/fixed-layout-editor\";\n\n// #region FormNestedFieldsContext\nexport type FormNestedFieldsContextType = {\n  parentNamePath: string;\n};\n\nexport const FormNestedFieldsContext = createContext<FormNestedFieldsContextType>({\n  parentNamePath: \"\",\n});\n\nexport const FormNestedFieldsContextProvider = FormNestedFieldsContext.Provider;\n\nexport const useFormNestedFieldsContext = () => {\n  const context = useContext(FormNestedFieldsContext);\n  if (!context) {\n    throw new Error(\"`FormNestedFieldsContext` must be used within a `FormNestedFieldsContextProvider`\");\n  }\n  return context;\n};\n// #endregion\n\n// #region NodeFormContext\nexport type NodeFormContextType = {\n  node: FlowNodeEntity;\n};\n\nexport const NodeFormContext = createContext<NodeFormContextType>({} as NodeFormContextType);\n\nexport const NodeFormContextProvider = NodeFormContext.Provider;\n\nexport const useNodeFormContext = () => {\n  const context = useContext(NodeFormContext);\n  if (!context) {\n    throw new Error(\"`NodeFormContext` must be used within a `NodeFormContextProvider`\");\n  }\n  return context;\n};\n// #endregion\n"
  },
  {
    "path": "ui/src/components/workflow/designer/forms/_shared.tsx",
    "content": "import { useEffect, useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { type FlowNodeEntity, useClientContext, useRefresh } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconChevronDown, IconEye, IconEyeOff, IconX } from \"@tabler/icons-react\";\nimport { useControllableValue } from \"ahooks\";\nimport { Anchor, type AnchorProps, App, Button, Drawer, Dropdown, Flex, type FormInstance, Space, Tooltip, Typography } from \"antd\";\nimport { isEqual } from \"radash\";\n\nimport Show from \"@/components/Show\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nimport { type NodeRegistry } from \"../nodes/typings\";\n\nexport interface NodeConfigDrawerProps {\n  children: React.ReactNode;\n  afterClose?: () => void;\n  anchor?: Pick<Required<AnchorProps>, \"items\"> | false;\n  footer?: boolean;\n  form: FormInstance;\n  loading?: boolean;\n  node: FlowNodeEntity;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nexport const NodeConfigDrawer = ({ children, afterClose, anchor, footer = true, form: formInst, loading, node, ...props }: NodeConfigDrawerProps) => {\n  const { t } = useTranslation();\n\n  const ctx = useClientContext();\n  const { playground } = ctx;\n\n  const refresh = useRefresh();\n\n  const { message, modal, notification } = App.useApp();\n\n  const [open, setOpen] = useControllableValue<boolean>(props, {\n    valuePropName: \"open\",\n    defaultValuePropName: \"defaultOpen\",\n    trigger: \"onOpenChange\",\n  });\n\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  const [formPending, setFormPending] = useState(false);\n\n  const submitForm = async () => {\n    let formValues: Record<string, unknown>;\n\n    setFormPending(true);\n    try {\n      formValues = await formInst.validateFields();\n    } catch (err) {\n      message.warning(t(\"common.errmsg.form_invalid\"));\n\n      setFormPending(false);\n      throw err;\n    }\n\n    try {\n      node.form!.setValueIn(\"config\", formValues);\n      node.form!.validate();\n    } catch (err) {\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n\n      throw err;\n    } finally {\n      setFormPending(false);\n    }\n  };\n\n  const nodeRegistry = node?.getNodeRegistry<NodeRegistry>();\n  const NodeIcon = nodeRegistry?.meta?.icon;\n  const renderNodeIcon = () =>\n    NodeIcon == null ? null : (\n      <div\n        className=\"flex size-6 items-center justify-center rounded-lg bg-white text-primary shadow-md dark:bg-stone-200\"\n        style={{\n          color: nodeRegistry?.meta?.iconColor,\n          backgroundColor: nodeRegistry?.meta?.iconBgColor,\n        }}\n      >\n        <NodeIcon size=\"1em\" color={nodeRegistry?.meta?.iconColor} stroke=\"1.25\" />\n      </div>\n    );\n\n  const [isNodeDisabled, setIsNodeDisabled] = useState(() => {\n    if (node) {\n      return node.form?.getValueIn<boolean>(\"disabled\");\n    }\n    return false;\n  });\n  useEffect(() => {\n    const d1 = playground.config.onDataChange(() => refresh());\n    const d2 = node?.onDataChange(() => setIsNodeDisabled(node.form?.getValueIn<boolean>(\"disabled\")));\n\n    return () => {\n      d1.dispose();\n      d2.dispose();\n    };\n  });\n\n  const handleOkClick = async () => {\n    if (node == null) {\n      setOpen(false);\n      return;\n    }\n\n    await submitForm();\n    setOpen(false);\n  };\n\n  const handleOkAndContinueClick = async () => {\n    if (node == null) {\n      setOpen(false);\n      return;\n    }\n\n    await submitForm();\n    message.success(t(\"common.text.saved\"));\n  };\n\n  const handleCancelClick = () => {\n    if (formPending) return;\n\n    setOpen(false);\n  };\n\n  const handleClose = () => {\n    if (formPending) return;\n\n    const picker = (obj: Record<string, unknown>) => {\n      return Object.entries(obj).reduce(\n        (acc, [key, value]) => {\n          const isEmpty =\n            value == null ||\n            (typeof value === \"string\" && value === \"\") ||\n            (Array.isArray(value) && value.length === 0) ||\n            (typeof value === \"object\" && !Array.isArray(value) && Object.keys(value).length === 0);\n\n          if (!isEmpty) {\n            acc[key] = value;\n          }\n\n          return acc;\n        },\n        {} as Record<string, unknown>\n      );\n    };\n    const oldValues = picker(node?.toJSON()?.data?.config ?? {});\n    const newValues = picker(formInst.getFieldsValue(true));\n    const changed = !isEqual(oldValues, {}) && !isEqual(oldValues, newValues);\n\n    const { promise, resolve } = Promise.withResolvers();\n    if (changed) {\n      modal.confirm({\n        title: t(\"common.text.operation_confirm\"),\n        content: t(\"workflow.detail.design.unsaved_changes.confirm\"),\n        onOk: () => resolve(void 0),\n      });\n    } else {\n      resolve(void 0);\n    }\n\n    promise.then(() => setOpen(false));\n  };\n\n  const handleDisableNodeClick = () => {\n    node.form!.setValueIn(\"disabled\", !isNodeDisabled);\n  };\n\n  return (\n    <Drawer\n      styles={{\n        header: {\n          paddingBottom: anchor ? 0 : void 0,\n        },\n      }}\n      afterOpenChange={(open) => !open && afterClose?.()}\n      autoFocus\n      closeIcon={false}\n      destroyOnHidden\n      footer={\n        footer ? (\n          <Flex className=\"px-2\" justify=\"end\" gap=\"small\">\n            <Button onClick={handleCancelClick}>{t(\"common.button.cancel\")}</Button>\n            <Space.Compact>\n              <Button loading={formPending} type=\"primary\" onClick={handleOkClick}>\n                {t(\"common.button.save\")}\n              </Button>\n              <Dropdown\n                menu={{\n                  items: [\n                    {\n                      key: \"save_and_continue\",\n                      label: t(\"common.button.save_and_continue\"),\n                      onClick: handleOkAndContinueClick,\n                    },\n                  ],\n                }}\n                placement=\"bottomRight\"\n                trigger={[\"click\"]}\n              >\n                <Button disabled={formPending} icon={<IconChevronDown size=\"1.25em\" />} type=\"primary\" />\n              </Dropdown>\n            </Space.Compact>\n          </Flex>\n        ) : (\n          <></>\n        )\n      }\n      forceRender={false}\n      loading={loading}\n      maskClosable={!formPending}\n      open={open}\n      size=\"large\"\n      title={\n        <>\n          <Flex align=\"center\" justify=\"space-between\" gap=\"small\">\n            <div>{renderNodeIcon()}</div>\n            <div className=\"flex-1 truncate\">{node?.toJSON()?.data?.name}</div>\n            <Show when={!!node && !nodeRegistry?.meta?.isStart && !nodeRegistry?.meta?.isNodeEnd}>\n              <Tooltip\n                title={isNodeDisabled ? t(\"workflow.detail.design.drawer.disabled.on.tooltip\") : t(\"workflow.detail.design.drawer.disabled.off.tooltip\")}\n              >\n                <Button\n                  className=\"ant-drawer-close\"\n                  style={{ marginInline: 0 }}\n                  disabled={playground.config.readonlyOrDisabled}\n                  icon={isNodeDisabled ? <IconEyeOff size=\"1.25em\" /> : <IconEye size=\"1.25em\" />}\n                  size=\"small\"\n                  type=\"text\"\n                  onClick={handleDisableNodeClick}\n                />\n              </Tooltip>\n            </Show>\n            <Button className=\"ant-drawer-close\" style={{ marginInline: 0 }} icon={<IconX size=\"1.25em\" />} size=\"small\" type=\"text\" onClick={handleClose} />\n          </Flex>\n\n          <div className=\"mt-3 truncate text-sm font-normal\">\n            <Typography.Text className=\"text-xs\" type=\"secondary\">\n              <span>{t(\"workflow.detail.design.drawer.node_id.label\")}</span>\n              <span>{node?.id}</span>\n            </Typography.Text>\n          </div>\n\n          <Show when={!!anchor}>\n            <div className=\"-mx-0.5 mt-3 text-sm font-normal\">\n              <Anchor\n                affix={false}\n                getContainer={() => containerRef.current!}\n                direction=\"horizontal\"\n                items={(anchor as AnchorProps).items}\n                onClick={(e, link) => {\n                  // https://github.com/ant-design/ant-design/issues/10577\n                  // https://github.com/ant-design/ant-design/issues/15326\n                  e.preventDefault();\n\n                  // 锚点元素需同时包含 `id` 和 `data-anchor` 两个属性\n                  const el = document.querySelector(`[data-anchor=\"${link.href.replace(/^#/g, \"\")}\"]`);\n                  el?.scrollIntoView({ block: \"start\", behavior: \"smooth\" });\n                }}\n              />\n            </div>\n          </Show>\n        </>\n      }\n      onClose={handleClose}\n    >\n      <div ref={containerRef} style={{ height: \"100%\", overflowX: \"hidden\", overflowY: \"auto\" }}>\n        {children}\n      </div>\n    </Drawer>\n  );\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/index.ts",
    "content": "﻿import Designer from \"./Designer\";\nimport NodeDrawer from \"./NodeDrawer\";\nimport Toolbar from \"./Toolbar\";\n\nexport { type DesignerInstance as WorkflowDesignerInstance, type DesignerProps as WorkflowDesignerProps } from \"./Designer\";\nexport const WorkflowDesigner = Designer;\n\nexport { type NodeDrawerProps as WorkflowNodeDrawerProps } from \"./NodeDrawer\";\nexport const WorkflowNodeDrawer = NodeDrawer;\n\nexport { type ToolbarProps as WorkflowToolbarProps } from \"./Toolbar\";\nexport const WorkflowToolbar = Toolbar;\n\nexport type * from \"./nodes/typings\";\n"
  },
  {
    "path": "ui/src/components/workflow/designer/nodes/BizApplyNodeRegistry.tsx",
    "content": "import { getI18n } from \"react-i18next\";\nimport { FeedbackLevel, Field } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconContract } from \"@tabler/icons-react\";\nimport { Avatar } from \"antd\";\n\nimport { acmeDns01ProvidersMap, acmeHttp01ProvidersMap } from \"@/domain/provider\";\nimport { type WorkflowNodeConfigForBizApply, newNode } from \"@/domain/workflow\";\n\nimport { BaseNode } from \"./_shared\";\nimport { NodeKindType, type NodeRegistry, NodeType } from \"./typings\";\nimport BizApplyNodeConfigForm from \"../forms/BizApplyNodeConfigForm\";\n\nexport const BizApplyNodeRegistry: NodeRegistry = {\n  type: NodeType.BizApply,\n\n  kind: NodeKindType.Business,\n\n  meta: {\n    labelText: getI18n().t(\"workflow_node.apply.label\"),\n\n    icon: IconContract,\n    iconColor: \"#fff\",\n    iconBgColor: \"#5b65f5\",\n\n    clickable: true,\n    expandable: false,\n  },\n\n  formMeta: {\n    validate: {\n      [\"config\"]: ({ value }) => {\n        const res = BizApplyNodeConfigForm.getSchema({}).safeParse(value);\n        if (!res.success) {\n          return {\n            message: res.error.message,\n            level: FeedbackLevel.Error,\n          };\n        }\n      },\n    },\n\n    render: () => {\n      const { t } = getI18n();\n\n      type MapValueType<M> = M extends Map<string, infer V> ? V : never;\n      const acmeProvidersMap = new Map<string, MapValueType<typeof acmeDns01ProvidersMap | typeof acmeHttp01ProvidersMap>>([\n        ...acmeDns01ProvidersMap,\n        ...acmeHttp01ProvidersMap,\n      ]);\n\n      return (\n        <BaseNode\n          description={\n            <div className=\"flex items-center justify-between gap-1\">\n              <Field<WorkflowNodeConfigForBizApply> name=\"config\">\n                {({ field: { value } }) => {\n                  const displayText = value.identifier === \"domain\" ? value.domains : value.identifier === \"ip\" ? value.ipaddrs : void 0;\n                  return <div className=\"flex-1 truncate\">{displayText || t(\"workflow.detail.design.editor.placeholder\")}</div>;\n                }}\n              </Field>\n              <Field<string> name=\"config.provider\">\n                {({ field: { value } }) => (value ? <Avatar shape=\"square\" src={acmeProvidersMap.get(value)?.icon} size={20} /> : <></>)}\n              </Field>\n            </div>\n          }\n        />\n      );\n    },\n  },\n\n  onAdd: () => {\n    return newNode(NodeType.BizApply, { i18n: getI18n() });\n  },\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/nodes/BizDeployNodeRegistry.tsx",
    "content": "import { getI18n } from \"react-i18next\";\nimport { FeedbackLevel, Field } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconPackage } from \"@tabler/icons-react\";\nimport { Avatar } from \"antd\";\n\nimport { deploymentProvidersMap } from \"@/domain/provider\";\nimport { newNode } from \"@/domain/workflow\";\n\nimport { getAllPreviousNodes } from \"../_util\";\nimport { BaseNode } from \"./_shared\";\nimport { NodeKindType, type NodeRegistry, NodeType } from \"./typings\";\nimport BizDeployNodeConfigForm from \"../forms/BizDeployNodeConfigForm\";\n\nexport const BizDeployNodeRegistry: NodeRegistry = {\n  type: NodeType.BizDeploy,\n\n  kind: NodeKindType.Business,\n\n  meta: {\n    labelText: getI18n().t(\"workflow_node.deploy.label\"),\n\n    icon: IconPackage,\n    iconColor: \"#fff\",\n    iconBgColor: \"#5b65f5\",\n\n    clickable: true,\n    expandable: false,\n  },\n\n  formMeta: {\n    validate: {\n      [\"config\"]: ({ value }) => {\n        const res = BizDeployNodeConfigForm.getSchema({}).safeParse(value);\n        if (!res.success) {\n          return {\n            message: res.error.message,\n            level: FeedbackLevel.Error,\n          };\n        }\n      },\n      [\"config.certificateOutputNodeId\"]: ({ value, context: { node } }) => {\n        if (value == null) return;\n\n        const prevNodeIds = getAllPreviousNodes(node).map((e) => e.id);\n        if (!prevNodeIds.includes(value)) {\n          return {\n            message: \"Invalid input\",\n            level: FeedbackLevel.Error,\n          };\n        }\n      },\n    },\n\n    render: () => {\n      const { t } = getI18n();\n\n      return (\n        <BaseNode\n          description={\n            <div className=\"flex items-center justify-between gap-1\">\n              <Field<string> name=\"config.provider\">\n                {({ field: { value } }) => (\n                  <>\n                    {value ? (\n                      <>\n                        <div className=\"flex-1 truncate\">{t(deploymentProvidersMap.get(value)?.name ?? \"\")}</div>\n                        <Avatar shape=\"square\" src={deploymentProvidersMap.get(value)?.icon} size={20} />\n                      </>\n                    ) : (\n                      t(\"workflow.detail.design.editor.placeholder\")\n                    )}\n                  </>\n                )}\n              </Field>\n            </div>\n          }\n        />\n      );\n    },\n  },\n\n  onAdd: () => {\n    return newNode(NodeType.BizDeploy, { i18n: getI18n() });\n  },\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/nodes/BizMonitorNodeRegistry.tsx",
    "content": "import { getI18n } from \"react-i18next\";\nimport { FeedbackLevel, Field } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconDeviceDesktopSearch } from \"@tabler/icons-react\";\n\nimport { newNode } from \"@/domain/workflow\";\n\nimport { BaseNode } from \"./_shared\";\nimport { NodeKindType, type NodeRegistry, NodeType } from \"./typings\";\nimport BizMonitorNodeConfigForm from \"../forms/BizMonitorNodeConfigForm\";\n\nexport const BizMonitorNodeRegistry: NodeRegistry = {\n  type: NodeType.BizMonitor,\n\n  kind: NodeKindType.Business,\n\n  meta: {\n    labelText: getI18n().t(\"workflow_node.monitor.label\"),\n\n    icon: IconDeviceDesktopSearch,\n    iconColor: \"#fff\",\n    iconBgColor: \"#5b65f5\",\n\n    clickable: true,\n    expandable: false,\n  },\n\n  formMeta: {\n    validate: {\n      [\"config\"]: ({ value }) => {\n        const res = BizMonitorNodeConfigForm.getSchema({}).safeParse(value);\n        if (!res.success) {\n          return {\n            message: res.error.message,\n            level: FeedbackLevel.Error,\n          };\n        }\n      },\n    },\n\n    render: () => {\n      const { t } = getI18n();\n\n      return (\n        <BaseNode\n          description={\n            <Field name=\"config.domain\">\n              {({ field: { value: fieldDomain } }) => (\n                <Field name=\"config.host\">\n                  {({ field: { value: fieldHost } }) => (\n                    <>{fieldDomain || fieldHost ? fieldDomain || fieldHost : t(\"workflow.detail.design.editor.placeholder\")}</>\n                  )}\n                </Field>\n              )}\n            </Field>\n          }\n        />\n      );\n    },\n  },\n\n  onAdd: () => {\n    return newNode(NodeType.BizMonitor, { i18n: getI18n() });\n  },\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/nodes/BizNotifyNodeRegistry.tsx",
    "content": "import { getI18n } from \"react-i18next\";\nimport { FeedbackLevel, Field } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconSend } from \"@tabler/icons-react\";\nimport { Avatar } from \"antd\";\n\nimport { notificationProvidersMap } from \"@/domain/provider\";\nimport { newNode } from \"@/domain/workflow\";\n\nimport { BaseNode } from \"./_shared\";\nimport { NodeKindType, type NodeRegistry, NodeType } from \"./typings\";\nimport BizNotifyNodeConfigForm from \"../forms/BizNotifyNodeConfigForm\";\n\nexport const BizNotifyNodeRegistry: NodeRegistry = {\n  type: NodeType.BizNotify,\n\n  kind: NodeKindType.Business,\n\n  meta: {\n    labelText: getI18n().t(\"workflow_node.notify.label\"),\n\n    icon: IconSend,\n    iconColor: \"#fff\",\n    iconBgColor: \"#0693d4\",\n\n    clickable: true,\n    expandable: false,\n  },\n\n  formMeta: {\n    validate: {\n      [\"config\"]: ({ value }) => {\n        const res = BizNotifyNodeConfigForm.getSchema({}).safeParse(value);\n        if (!res.success) {\n          return {\n            message: res.error.message,\n            level: FeedbackLevel.Error,\n          };\n        }\n      },\n    },\n\n    render: () => {\n      const { t } = getI18n();\n\n      return (\n        <BaseNode\n          description={\n            <div className=\"flex items-center justify-between gap-1\">\n              <Field<string> name=\"config.provider\">\n                {({ field: { value } }) => (\n                  <>\n                    {value ? (\n                      <>\n                        <div className=\"flex-1 truncate\">{t(notificationProvidersMap.get(value)?.name ?? \"\")}</div>\n                        <Avatar shape=\"square\" src={notificationProvidersMap.get(value)?.icon} size={20} />\n                      </>\n                    ) : (\n                      t(\"workflow.detail.design.editor.placeholder\")\n                    )}\n                  </>\n                )}\n              </Field>\n            </div>\n          }\n        />\n      );\n    },\n  },\n\n  onAdd: () => {\n    return newNode(NodeType.BizNotify, { i18n: getI18n() });\n  },\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/nodes/BizUploadNodeRegistry.tsx",
    "content": "import { getI18n } from \"react-i18next\";\nimport { FeedbackLevel, Field } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconCloudUpload } from \"@tabler/icons-react\";\n\nimport { newNode } from \"@/domain/workflow\";\nimport { getCertificateSubjectAltNames as getX509SubjectAltNames } from \"@/utils/x509\";\n\nimport { BaseNode } from \"./_shared\";\nimport { NodeKindType, type NodeRegistry, NodeType } from \"./typings\";\nimport BizUploadNodeConfigForm from \"../forms/BizUploadNodeConfigForm\";\n\nexport const BizUploadNodeRegistry: NodeRegistry = {\n  type: NodeType.BizUpload,\n\n  kind: NodeKindType.Business,\n\n  meta: {\n    labelText: getI18n().t(\"workflow_node.upload.label\"),\n\n    icon: IconCloudUpload,\n    iconColor: \"#fff\",\n    iconBgColor: \"#5b65f5\",\n\n    clickable: true,\n    expandable: false,\n  },\n\n  formMeta: {\n    validate: {\n      [\"config\"]: ({ value }) => {\n        const res = BizUploadNodeConfigForm.getSchema({}).safeParse(value);\n        if (!res.success) {\n          return {\n            message: res.error.message,\n            level: FeedbackLevel.Error,\n          };\n        }\n      },\n    },\n\n    render: () => {\n      const { t } = getI18n();\n\n      return (\n        <BaseNode\n          description={\n            <Field<string> name=\"config.source\">\n              {({ field: { value: fieldSource } }) => (\n                <>\n                  {fieldSource == null || fieldSource === \"\" || fieldSource === \"form\" ? (\n                    <Field<string> name=\"config.certificate\">\n                      {({ field: { value: fieldCertificate } }) => {\n                        const displayText = fieldCertificate ? getX509SubjectAltNames(fieldCertificate).join(\";\") : void 0;\n                        return <>{displayText || t(\"workflow.detail.design.editor.placeholder\")}</>;\n                      }}\n                    </Field>\n                  ) : (\n                    <Field<string> name=\"config.certificate\">\n                      {({ field: { value: fieldCertificate } }) => <>{fieldCertificate || t(\"workflow.detail.design.editor.placeholder\")}</>}\n                    </Field>\n                  )}\n                </>\n              )}\n            </Field>\n          }\n        />\n      );\n    },\n  },\n\n  onAdd: () => {\n    return newNode(NodeType.BizUpload, { i18n: getI18n() });\n  },\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/nodes/ConditionNode.tsx",
    "content": "import { getI18n } from \"react-i18next\";\nimport { FeedbackLevel, Field, FlowNodeBaseType, FlowNodeSplitType } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconFilter, IconFilterFilled, IconSitemap } from \"@tabler/icons-react\";\nimport { Typography } from \"antd\";\n\nimport { type Expr, ExprType, newNode } from \"@/domain/workflow\";\n\nimport { getAllPreviousNodes } from \"../_util\";\nimport { BaseNode, BranchNode } from \"./_shared\";\nimport { NodeKindType, type NodeRegistry, NodeType } from \"./typings\";\nimport BranchBlockNodeConfigForm from \"../forms/BranchBlockNodeConfigForm\";\n\nexport const ConditionNodeRegistry: NodeRegistry = {\n  type: NodeType.Condition,\n\n  kind: NodeKindType.Logic,\n\n  extend: FlowNodeSplitType.DYNAMIC_SPLIT,\n\n  meta: {\n    labelText: getI18n().t(\"workflow_node.condition.label\"),\n\n    icon: IconSitemap,\n    iconColor: \"#fff\",\n    iconBgColor: \"#373c43\",\n\n    clickable: false,\n    expandable: false,\n\n    deleteDisable: false,\n  },\n\n  formMeta: {\n    render: () => {\n      return <BaseNode />;\n    },\n  },\n\n  onAdd() {\n    return newNode(NodeType.Condition, { i18n: getI18n() });\n  },\n};\n\nexport const BranchBlockNodeRegistry: NodeRegistry = {\n  type: NodeType.BranchBlock,\n\n  kind: NodeKindType.Logic,\n\n  extend: FlowNodeBaseType.BLOCK,\n\n  meta: {\n    labelText: getI18n().t(\"workflow_node.branch_block.label\"),\n\n    icon: IconSitemap,\n    iconColor: \"#fff\",\n    iconBgColor: \"#373c43\",\n\n    clickable: true,\n\n    addDisable: true,\n    copyDisable: true,\n  },\n\n  formMeta: {\n    validate: {\n      [\"config\"]: ({ value }) => {\n        const res = BranchBlockNodeConfigForm.getSchema({}).safeParse(value);\n        if (!res.success) {\n          return {\n            message: res.error.message,\n            level: FeedbackLevel.Error,\n          };\n        }\n      },\n      [\"config.expression\"]: ({ value, context: { node } }) => {\n        if (value == null) return;\n\n        const prevNodeIds = getAllPreviousNodes(node).map((e) => e.id);\n        const deepValidate = (expr: Expr) => {\n          if (\"selector\" in expr) {\n            if (!prevNodeIds.includes(expr.selector.id)) {\n              return false;\n            }\n          }\n\n          if (\"left\" in expr) {\n            if (!deepValidate(expr.left)) {\n              return false;\n            }\n          }\n\n          if (\"right\" in expr) {\n            if (!deepValidate(expr.right)) {\n              return false;\n            }\n          }\n\n          return true;\n        };\n        if (!deepValidate(value)) {\n          return {\n            message: \"Invalid input\",\n            level: FeedbackLevel.Error,\n          };\n        }\n      },\n    },\n\n    render: () => {\n      const { t } = getI18n();\n\n      return (\n        <BranchNode\n          description={\n            <>\n              <div className=\"flex items-center justify-center gap-2\">\n                <div className=\"flex items-center justify-center\">\n                  <Field<Expr> name=\"config.expression\">\n                    {({ field: { value } }) => (\n                      <>\n                        {value == null ? (\n                          <IconFilter size=\"1.25em\" stroke=\"1.25\" />\n                        ) : (\n                          <IconFilterFilled color=\"var(--color-primary)\" size=\"1.25em\" stroke=\"1.25\" />\n                        )}\n                      </>\n                    )}\n                  </Field>\n                </div>\n                <div className=\"truncate\">\n                  <Field<string> name=\"name\">{({ field: { value } }) => <>{value || \"\\u00A0\"}</>}</Field>\n                </div>\n              </div>\n              <div className=\"mt-1\">\n                <div className=\"truncate\">\n                  <Field<Expr> name=\"config.expression\">\n                    {({ field: { value } }) => (\n                      <Typography.Text className=\"text-xs\" type=\"secondary\">\n                        {value == null\n                          ? t(\"workflow_node.branch_block.state.no\")\n                          : value.type === ExprType.Logical && value.operator === \"and\"\n                            ? t(\"workflow_node.branch_block.state.and\")\n                            : t(\"workflow_node.branch_block.state.or\")}\n                      </Typography.Text>\n                    )}\n                  </Field>\n                </div>\n              </div>\n            </>\n          }\n        />\n      );\n    },\n  },\n\n  canAdd: () => {\n    return false;\n  },\n\n  canDelete: (_, node) => {\n    return node.parent != null && node.parent.blocks.length >= 2;\n  },\n\n  onAdd(_, from) {\n    const node = newNode(NodeType.BranchBlock, { i18n: getI18n() });\n    if (from != null) {\n      const siblingLength = from.blocks?.find((b) => b.isInlineBlocks)?.blocks?.length;\n      if (siblingLength != null) {\n        node.data.name = `${node.data.name} ${siblingLength + 1}`;\n      }\n    }\n\n    return node;\n  },\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/nodes/DelayNode.tsx",
    "content": "import { getI18n } from \"react-i18next\";\nimport { FeedbackLevel, Field } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconHourglassHigh } from \"@tabler/icons-react\";\n\nimport { newNode } from \"@/domain/workflow\";\n\nimport { BaseNode } from \"./_shared\";\nimport { NodeKindType, type NodeRegistry, NodeType } from \"./typings\";\nimport DelayNodeConfigForm from \"../forms/DelayNodeConfigForm\";\n\nexport const DelayNodeRegistry: NodeRegistry = {\n  type: NodeType.Delay,\n\n  kind: NodeKindType.Basis,\n\n  meta: {\n    labelText: getI18n().t(\"workflow_node.delay.label\"),\n\n    icon: IconHourglassHigh,\n    iconColor: \"#2a354c\",\n    iconBgColor: \"#fed421\",\n\n    clickable: true,\n    expandable: false,\n  },\n\n  formMeta: {\n    validate: {\n      [\"config\"]: ({ value }) => {\n        const res = DelayNodeConfigForm.getSchema({}).safeParse(value);\n        if (!res.success) {\n          return {\n            message: res.error.message,\n            level: FeedbackLevel.Error,\n          };\n        }\n      },\n    },\n\n    render: () => {\n      const { t } = getI18n();\n\n      return (\n        <BaseNode\n          description={\n            <Field name=\"config.wait\">\n              {({ field: { value } }) => (\n                <>\n                  <div>{value != null ? `${value} ${t(\"workflow_node.delay.form.wait.unit\")}` : t(\"workflow.detail.design.editor.placeholder\")}</div>\n                </>\n              )}\n            </Field>\n          }\n        />\n      );\n    },\n  },\n\n  onAdd() {\n    return newNode(NodeType.Delay, { i18n: getI18n() });\n  },\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/nodes/EndNode.tsx",
    "content": "import { getI18n } from \"react-i18next\";\nimport { FlowNodeBaseType } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconLogout } from \"@tabler/icons-react\";\n\nimport { newNode } from \"@/domain/workflow\";\n\nimport { BaseNode } from \"./_shared\";\nimport { NodeKindType, type NodeRegistry, NodeType } from \"./typings\";\n\nexport const EndNodeRegistry: NodeRegistry = {\n  type: NodeType.End,\n\n  kind: NodeKindType.Basis,\n\n  meta: {\n    labelText: getI18n().t(\"workflow_node.end.label\"),\n\n    icon: IconLogout,\n    iconColor: \"#fff\",\n    iconBgColor: \"#336df4\",\n\n    isNodeEnd: true,\n\n    clickable: false,\n    expandable: false,\n    selectable: false,\n\n    copyDisable: true,\n  },\n\n  formMeta: {\n    render: () => {\n      return <BaseNode />;\n    },\n  },\n\n  canAdd(_, from) {\n    // You can only add to the last node of the branch\n    if (!from.isLast) return false;\n\n    // `originParent` can determine whether it is condition, and then determine whether it is the last one\n    // https://github.com/bytedance/flowgram.ai/pull/146\n    if (from.parent && from.parent.parent?.flowNodeType === FlowNodeBaseType.INLINE_BLOCKS && from.parent.originParent && !from.parent.originParent.isLast) {\n      const allBranches = from.parent.parent!.blocks;\n      // Determine whether the last node of all branch is end, all branches are not allowed to be end\n      const branchEndCount = allBranches.filter((block) => block.blocks[block.blocks.length - 1]?.getNodeMeta().isNodeEnd).length;\n      return branchEndCount < allBranches.length - 1;\n    }\n\n    return true;\n  },\n\n  canDelete(ctx, node) {\n    return node.parent !== ctx.document.root;\n  },\n\n  onAdd() {\n    return newNode(NodeType.End, { i18n: getI18n() });\n  },\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/nodes/StartNode.tsx",
    "content": "import { getI18n } from \"react-i18next\";\nimport { FeedbackLevel, Field } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconRocket } from \"@tabler/icons-react\";\n\nimport { WORKFLOW_TRIGGERS } from \"@/domain/workflow\";\n\nimport { BaseNode } from \"./_shared\";\nimport { NodeKindType, type NodeRegistry, NodeType } from \"./typings\";\nimport StartNodeConfigForm from \"../forms/StartNodeConfigForm\";\n\nexport const StartNodeRegistry: NodeRegistry = {\n  type: NodeType.Start,\n\n  kind: NodeKindType.Basis,\n\n  meta: {\n    labelText: getI18n().t(\"workflow_node.start.label\"),\n\n    icon: IconRocket,\n    iconColor: \"#fff\",\n    iconBgColor: \"#ed6d0c\",\n\n    isStart: true,\n\n    clickable: true,\n    expandable: false,\n    selectable: false,\n\n    addDisable: true,\n    copyDisable: true,\n    deleteDisable: true,\n  },\n\n  formMeta: {\n    validate: {\n      [\"config\"]: ({ value }) => {\n        const res = StartNodeConfigForm.getSchema({}).safeParse(value);\n        if (!res.success) {\n          return {\n            message: res.error.message,\n            level: FeedbackLevel.Error,\n          };\n        }\n      },\n    },\n\n    render: () => {\n      const { t } = getI18n();\n\n      return (\n        <BaseNode\n          description={\n            <div className=\"flex items-center justify-between gap-1\">\n              <Field name=\"config.trigger\">\n                {({ field: { value: fieldTrigger } }) => (\n                  <>\n                    <div>\n                      {fieldTrigger === WORKFLOW_TRIGGERS.SCHEDULED\n                        ? t(\"workflow.props.trigger.scheduled\")\n                        : fieldTrigger === WORKFLOW_TRIGGERS.MANUAL\n                          ? t(\"workflow.props.trigger.manual\")\n                          : t(\"workflow.detail.design.editor.placeholder\")}\n                    </div>\n                    <div>\n                      <Field name=\"config.triggerCron\">\n                        {({ field: { value: fieldTriggerCron } }) => <>{fieldTrigger === WORKFLOW_TRIGGERS.SCHEDULED ? fieldTriggerCron || \"\\u00A0\" : \"\"}</>}\n                      </Field>\n                    </div>\n                  </>\n                )}\n              </Field>\n            </div>\n          }\n        />\n      );\n    },\n  },\n\n  canAdd: () => {\n    return false;\n  },\n\n  canDelete: () => {\n    return false;\n  },\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/nodes/TryCatchNode.tsx",
    "content": "﻿import { getI18n } from \"react-i18next\";\nimport { Field } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconArrowsSplit, IconCircleX } from \"@tabler/icons-react\";\n\nimport { newNode } from \"@/domain/workflow\";\n\nimport { BaseNode, BranchNode } from \"./_shared\";\nimport { NodeKindType, type NodeRegistry, NodeType } from \"./typings\";\n\nexport const TryCatchNodeRegistry: NodeRegistry = {\n  type: NodeType.TryCatch,\n\n  kind: NodeKindType.Logic,\n\n  meta: {\n    labelText: getI18n().t(\"workflow_node.try_catch.label\"),\n\n    icon: IconArrowsSplit,\n    iconColor: \"#fff\",\n    iconBgColor: \"#373c43\",\n\n    clickable: false,\n    expandable: false,\n  },\n\n  formMeta: {\n    render: () => {\n      return <BaseNode />;\n    },\n  },\n\n  onAdd() {\n    return newNode(NodeType.TryCatch, { i18n: getI18n() });\n  },\n};\n\nexport const CatchBlockNodeRegistry: NodeRegistry = {\n  type: NodeType.CatchBlock,\n\n  kind: NodeKindType.Logic,\n\n  meta: {\n    labelText: getI18n().t(\"workflow_node.catch_block.label\"),\n\n    clickable: false,\n    draggable: false,\n\n    addDisable: true,\n    copyDisable: true,\n  },\n\n  formMeta: {\n    render: () => {\n      return (\n        <BranchNode\n          description={\n            <div className=\"flex items-center justify-center gap-2\">\n              <div className=\"flex items-center justify-center\">\n                <IconCircleX color=\"var(--color-error)\" size=\"1.25em\" stroke=\"1.25\" />\n              </div>\n              <div className=\"truncate\">\n                <Field<string> name=\"name\">{({ field: { value } }) => <>{value || \"\\u00A0\"}</>}</Field>\n              </div>\n            </div>\n          }\n        />\n      );\n    },\n  },\n\n  canAdd: () => false,\n\n  canDelete: (_, node) => {\n    return node.parent != null && node.parent.blocks.length >= 2;\n  },\n\n  onAdd() {\n    return newNode(NodeType.CatchBlock, { i18n: getI18n() });\n  },\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/nodes/_example.ts",
    "content": "﻿import { type FlowNodeRegistry } from \"@flowgram.ai/fixed-layout-editor\";\nimport { nanoid } from \"nanoid\";\n\n/**\n * 自定义节点注册\n */\nexport const nodeRegistry: FlowNodeRegistry = {\n  /**\n   * 自定义节点类型\n   */\n  type: \"ifElse\",\n\n  /**\n   * 自定义节点扩展:\n   *  - loop: 扩展为循环节点\n   *  - start: 扩展为开始节点\n   *  - dynamicSplit: 扩展为分支节点\n   *  - end: 扩展为结束节点\n   *  - tryCatch: 扩展为 tryCatch 节点\n   *  - break: 分支断开\n   *  - default: 扩展为普通节点 (默认)\n   */\n  extend: \"dynamicSplit\",\n\n  /**\n   * 节点配置信息\n   */\n  meta: {\n    isStart: false, // 是否为开始节点\n    isNodeEnd: false, // 是否为结束节点，结束节点后边无法再添加节点\n    draggable: false, // 是否可拖拽，如开始节点和结束节点无法拖拽\n    selectable: false, // 触发器等开始节点不能被框选\n    deleteDisable: true, // 禁止删除\n    copyDisable: true, // 禁止克隆\n    addDisable: true, // 禁止添加\n  },\n\n  onAdd() {\n    return {\n      id: `IfElse_${nanoid(5)}`,\n      type: \"ifElse\",\n      data: {\n        title: \"IfElse\",\n      },\n      blocks: [\n        {\n          id: nanoid(5),\n          type: \"block\",\n          data: {\n            title: \"If\",\n          },\n        },\n        {\n          id: nanoid(5),\n          type: \"block\",\n          data: {\n            title: \"Else\",\n          },\n        },\n      ],\n    };\n  },\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/nodes/_shared.tsx",
    "content": "import { startTransition, useEffect, useMemo, useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport {\n  Field,\n  type FlowNodeEntity,\n  FlowNodeRenderData,\n  type NodeRenderReturnType,\n  useClientContext,\n  useWatchFormState,\n  useWatchFormValueIn,\n} from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconCopy, IconDotsVertical, IconGripVertical, IconLabel, IconX } from \"@tabler/icons-react\";\nimport { Button, type ButtonProps, Card, Dropdown, Input, type InputRef, Popover, theme } from \"antd\";\n\nimport { mergeCls } from \"@/utils/css\";\n\nimport { type NodeJSON, type NodeRegistry } from \"./typings\";\nimport { duplicateNodeJSON } from \"../_util\";\nimport { useNodeRenderContext } from \"../NodeRenderContext\";\n\nconst useInternalRenamingInput = ({ nodeRender }: { nodeRender: NodeRenderReturnType }) => {\n  const inputRef = useRef<InputRef>(null);\n  const [inputVisible, setInputVisible] = useState(false);\n  const [inputValue, setInputValue] = useState(\"\");\n\n  const showInput = () => {\n    setInputVisible(true);\n    setInputValue(nodeRender.data?.name);\n    setTimeout(() => {\n      inputRef.current?.focus({ cursor: \"end\" });\n    }, 0);\n  };\n\n  const hideInput = () => {\n    setInputVisible(false);\n    setInputValue(nodeRender.data?.name);\n  };\n\n  const handleInputBlur = async (e: React.FocusEvent<HTMLInputElement>) => {\n    const value = e.target.value.trim();\n    if (!value || value === (nodeRender.data?.name || \"\")) {\n      setInputVisible(false);\n      return;\n    }\n\n    setInputVisible(false);\n\n    nodeRender.updateData({ ...nodeRender.data, name: value });\n  };\n\n  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setInputValue(e.target.value);\n  };\n\n  const handleInputMouseDown = (e: React.MouseEvent<HTMLInputElement>) => {\n    e.stopPropagation();\n  };\n\n  const handleInputMouseUp = (e: React.MouseEvent<HTMLInputElement>) => {\n    e.stopPropagation();\n  };\n\n  const handleInputPressEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    e.currentTarget.blur();\n  };\n\n  return {\n    inputRef: inputRef,\n    inputProps: {\n      value: inputValue,\n      onBlur: handleInputBlur,\n      onChange: handleInputChange,\n      onPressEnter: handleInputPressEnter,\n      onMouseDown: handleInputMouseDown,\n      onMouseUp: handleInputMouseUp,\n    },\n    visible: inputVisible,\n    value: inputValue,\n\n    show: showInput,\n    hide: hideInput,\n  };\n};\n\nconst InternalNodeCard = ({\n  className,\n  style,\n  children,\n  nodeRender,\n}: {\n  className?: string;\n  style?: React.CSSProperties;\n  children?: React.ReactNode;\n  nodeRender: NodeRenderReturnType;\n}) => {\n  const nodeRenderData = nodeRender.node.getData(FlowNodeRenderData)!;\n  const nodeRegistry = nodeRender.node.getNodeRegistry<NodeRegistry>();\n\n  const isActivated = useMemo(() => nodeRenderData.activated || nodeRenderData.lineActivated, [nodeRenderData.activated, nodeRenderData.lineActivated]);\n  const [isHovering, setIsHovering] = useState(false);\n  const [isNodeInvalid, setIsNodeInvalid] = useState(false);\n  const isNodeDisabled = useWatchFormValueIn(nodeRender.node, \"disabled\");\n\n  const formState = useWatchFormState(nodeRender.node);\n  useEffect(() => setIsNodeInvalid(!!formState?.invalid), [formState?.invalid]);\n\n  return (\n    <Card\n      className={mergeCls(\n        \"relative rounded-xl shadow-sm\",\n        { \"border-primary\": isActivated },\n        { \"border-dashed\": isNodeDisabled },\n        nodeRegistry.meta?.clickable ? \"cursor-pointer\" : \"cursor-default\",\n        className\n      )}\n      style={style}\n      styles={{ body: { padding: 0 } }}\n      hoverable\n      onMouseEnter={() => startTransition(() => setIsHovering(true))}\n      onMouseLeave={() => startTransition(() => setIsHovering(false))}\n    >\n      <div\n        className=\"relative z-1 transition-opacity\"\n        style={{\n          opacity: isHovering ? 1 : isNodeDisabled ? 0.3 : void 0,\n        }}\n      >\n        {children}\n      </div>\n      <div\n        className=\"absolute z-0 rounded-xl border-solid border-transparent transition-all duration-500\"\n        style={{\n          top: \"-1px\",\n          left: \"-1px\",\n          right: \"-1px\",\n          bottom: \"-1px\",\n          borderWidth: \"2px\",\n          borderColor: isHovering ? \"var(--color-primary)\" : isNodeInvalid ? \"var(--color-error)\" : void 0,\n          borderStyle: isNodeDisabled ? \"dashed\" : \"solid\",\n        }}\n      />\n    </Card>\n  );\n};\n\nconst InternalNodeMenuButton = ({\n  className,\n  style,\n  onMenuSelect,\n  ...props\n}: ButtonProps & {\n  onMenuSelect?: (key: \"rename\" | \"duplicate\" | \"remove\") => void;\n}) => {\n  const { t } = useTranslation();\n\n  const ctx = useClientContext();\n  const { operation, playground } = ctx;\n\n  const { node, ...nodeRender } = useNodeRenderContext();\n  const nodeRegistry = node.getNodeRegistry<NodeRegistry>();\n\n  const getLatestDuplicateDisabledState = () => {\n    if (nodeRegistry.meta?.copyDisable != null && nodeRegistry.meta.copyDisable) {\n      return true;\n    }\n    return false;\n  };\n  const getLatestRemoveDisabledState = () => {\n    if (nodeRegistry.meta?.deleteDisable != null && nodeRegistry.meta.deleteDisable) {\n      return true;\n    }\n    if (nodeRegistry.canDelete != null) {\n      return !nodeRegistry.canDelete(ctx, node);\n    }\n    return false;\n  };\n  const [menuDuplicateDisabled, setMenuDuplicateDisabled] = useState(() => getLatestDuplicateDisabledState());\n  const [menuRemoveDisabled, setMenuRemoveDisabled] = useState(() => getLatestRemoveDisabledState());\n  useEffect(() => {\n    // 这里不能使用 useMemo() 来决定 menuRemoveDisabled，因为依赖项没有发生改变（对象引用始终是同一个）\n    // 因此需要使用事件钩子来监听，并更新 menuRemoveDisabled 的状态\n    const callback = () => {\n      setMenuDuplicateDisabled(getLatestDuplicateDisabledState());\n      setMenuRemoveDisabled(getLatestRemoveDisabledState());\n    };\n    const d1 = node.onEntityChange(callback);\n    const d2 = node.parent?.onEntityChange?.(callback);\n\n    return () => {\n      d1?.dispose();\n      d2?.dispose();\n    };\n  }, []);\n\n  const handleClickRename = () => {\n    onMenuSelect?.(\"rename\");\n  };\n\n  const handleClickDuplicate = () => {\n    if (menuDuplicateDisabled) {\n      return;\n    }\n\n    const parent = node.originParent ?? node.parent;\n    if (parent != null) {\n      const nodeJSON = duplicateNodeJSON(node.toJSON() as NodeJSON);\n\n      let block: FlowNodeEntity;\n      if (nodeRender.isBlockOrderIcon) {\n        block = operation.addBlock(parent, nodeJSON);\n      } else {\n        block = operation.addFromNode(node, nodeJSON);\n      }\n\n      setTimeout(() => {\n        playground.scrollToView({\n          bounds: block.bounds,\n          scrollToCenter: true,\n        });\n      }, 1);\n\n      onMenuSelect?.(\"duplicate\");\n    }\n  };\n\n  const handleClickRemove = () => {\n    if (menuRemoveDisabled) {\n      return;\n    }\n\n    nodeRender.deleteNode();\n\n    onMenuSelect?.(\"remove\");\n  };\n\n  return playground.config.readonlyOrDisabled ? null : (\n    <Dropdown\n      styles={{\n        root: {\n          zIndex: 10 /* 确保要比 Minimap 组件层级要高，防止被遮挡而点击不到 */,\n        },\n      }}\n      arrow={false}\n      destroyOnHidden\n      menu={{\n        items: [\n          {\n            key: \"rename\",\n            label: nodeRender.isBlockOrderIcon ? t(\"workflow.detail.design.editor.rename_branch\") : t(\"workflow.detail.design.editor.rename_node\"),\n            icon: <IconLabel size=\"1em\" />,\n            onClick: handleClickRename,\n          },\n          {\n            key: \"duplicate\",\n            label: nodeRender.isBlockOrderIcon ? t(\"workflow.detail.design.editor.duplicate_branch\") : t(\"workflow.detail.design.editor.duplicate_node\"),\n            icon: <IconCopy size=\"1em\" />,\n            disabled: menuDuplicateDisabled,\n            onClick: handleClickDuplicate,\n          },\n          {\n            type: \"divider\",\n          },\n          {\n            key: \"remove\",\n            label: nodeRender.isBlockOrderIcon ? t(\"workflow.detail.design.editor.remove_branch\") : t(\"workflow.detail.design.editor.remove_node\"),\n            icon: <IconX size=\"1em\" />,\n            danger: true,\n            disabled: menuRemoveDisabled,\n            onClick: handleClickRemove,\n          },\n        ],\n        onClick: (e) => {\n          e.domEvent.stopPropagation();\n        },\n      }}\n      trigger={[\"click\"]}\n    >\n      <Button\n        className={className}\n        style={style}\n        icon={<IconDotsVertical color=\"grey\" size=\"1.25em\" />}\n        type=\"text\"\n        {...props}\n        onClick={(e) => {\n          e.stopPropagation();\n          props.onClick?.(e);\n        }}\n      />\n    </Dropdown>\n  );\n};\n\nexport interface BaseNodeProps {\n  className?: string;\n  style?: React.CSSProperties;\n  children?: React.ReactNode;\n  description?: React.ReactNode;\n}\n\nexport const BaseNode = ({ className, style, children, description }: BaseNodeProps) => {\n  const { token: themeToken } = theme.useToken();\n\n  const ctx = useClientContext();\n  const { playground } = ctx;\n\n  const nodeRender = useNodeRenderContext();\n  const nodeRegistry = nodeRender.node.getNodeRegistry<NodeRegistry>();\n\n  const NodeIcon = nodeRegistry.meta?.icon;\n  const renderNodeIcon = () => {\n    return NodeIcon == null ? null : (\n      <div\n        className=\"mr-2 flex size-9 items-center justify-center rounded-lg bg-white text-primary shadow-md dark:bg-stone-200\"\n        style={{\n          color: nodeRegistry.meta?.iconColor,\n          backgroundColor: nodeRegistry.meta?.iconBgColor,\n        }}\n      >\n        <NodeIcon size=\"1.75em\" color={nodeRegistry.meta?.iconColor} stroke=\"1.25\" />\n      </div>\n    );\n  };\n\n  const { inputRef, inputProps, visible: inputVisible, show: showInput } = useInternalRenamingInput({ nodeRender });\n\n  return (\n    <Popover\n      classNames={{ root: \"shadow-md\" }}\n      styles={{ container: { padding: 0 } }}\n      arrow={false}\n      content={\n        inputVisible ? null : (\n          <InternalNodeMenuButton\n            variant=\"text\"\n            onMenuSelect={(key) => {\n              switch (key) {\n                case \"rename\":\n                  showInput();\n                  break;\n              }\n            }}\n          />\n        )\n      }\n      placement=\"rightTop\"\n    >\n      <div\n        className=\"group/node relative\"\n        onClick={(e) => {\n          if (inputVisible) {\n            e.stopPropagation();\n          }\n        }}\n      >\n        <InternalNodeCard className={mergeCls(\"w-[320px]\", className)} style={style} nodeRender={nodeRender}>\n          {children != null ? (\n            children\n          ) : (\n            <div className={mergeCls(\"flex items-center gap-1 overflow-hidden p-3\", inputVisible ? \"invisible\" : \"visible\")}>\n              {renderNodeIcon()}\n              <div className=\"flex-1 overflow-hidden\">\n                <div className=\"truncate\">\n                  <Field<string> name=\"name\">{({ field: { value } }) => <>{value || \"\\u00A0\"}</>}</Field>\n                </div>\n                {description != null && (\n                  <div className=\"truncate text-xs\" style={{ color: themeToken.colorTextTertiary }}>\n                    {description}\n                  </div>\n                )}\n              </div>\n            </div>\n          )}\n        </InternalNodeCard>\n\n        {!playground.config.readonlyOrDisabled && nodeRegistry.meta?.draggable === true && (\n          <div className=\"absolute top-1/2 -left-4 z-1 hidden -translate-y-1/2 group-hover/node:block\">\n            <IconGripVertical size=\"1em\" stroke=\"1\" />\n          </div>\n        )}\n\n        {!playground.config.readonlyOrDisabled && (\n          <div className={mergeCls(\"absolute top-1/2 left-2 right-2 -translate-y-1/2 z-1\", inputVisible ? \"block\" : \"hidden\")}>\n            <Input ref={inputRef} maxLength={100} variant=\"underlined\" {...inputProps} />\n          </div>\n        )}\n      </div>\n    </Popover>\n  );\n};\n\nexport interface BranchNodeProps extends BaseNodeProps {}\n\nexport const BranchNode = ({ className, style, children, description }: BranchNodeProps) => {\n  const ctx = useClientContext();\n  const { playground } = ctx;\n\n  const nodeRender = useNodeRenderContext();\n  const nodeRegistry = nodeRender.node.getNodeRegistry<NodeRegistry>();\n\n  const { inputRef, inputProps, visible: inputVisible, show: showInput } = useInternalRenamingInput({ nodeRender });\n\n  return (\n    <Popover\n      classNames={{ root: \"shadow-md\" }}\n      styles={{ container: { padding: 0 } }}\n      arrow={false}\n      content={\n        inputVisible ? null : (\n          <InternalNodeMenuButton\n            variant=\"text\"\n            onMenuSelect={(key) => {\n              switch (key) {\n                case \"rename\":\n                  showInput();\n                  break;\n              }\n            }}\n          />\n        )\n      }\n      placement=\"rightTop\"\n    >\n      <div\n        className=\"group/node relative\"\n        onClick={(e) => {\n          if (inputVisible) {\n            e.stopPropagation();\n          }\n        }}\n      >\n        <InternalNodeCard className={mergeCls(\"w-[240px]\", className)} style={style} nodeRender={nodeRender}>\n          {children != null ? (\n            children\n          ) : (\n            <div className={mergeCls(\"overflow-hidden px-3 py-2\", inputVisible ? \"invisible\" : \"visible\")}>\n              <div className=\"truncate text-center\">\n                {description ?? <Field<string> name=\"name\">{({ field: { value } }) => <>{value || \"\\u00A0\"}</>}</Field>}\n              </div>\n            </div>\n          )}\n        </InternalNodeCard>\n\n        {!playground.config.readonlyOrDisabled && nodeRegistry.meta?.draggable === true && (\n          <div className=\"absolute top-1/2 -left-4 z-1 hidden -translate-y-1/2 group-hover/node:block\">\n            <IconGripVertical size=\"1em\" stroke=\"1\" />\n          </div>\n        )}\n\n        {!playground.config.readonlyOrDisabled && (\n          <div className={mergeCls(\"absolute top-1/2 left-2 right-2 -translate-y-1/2 z-1\", inputVisible ? \"block\" : \"hidden\")}>\n            <Input ref={inputRef} maxLength={100} variant=\"underlined\" {...inputProps} />\n          </div>\n        )}\n      </div>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "ui/src/components/workflow/designer/nodes/index.ts",
    "content": "﻿import { BizApplyNodeRegistry } from \"./BizApplyNodeRegistry\";\nimport { BizDeployNodeRegistry } from \"./BizDeployNodeRegistry\";\nimport { BizMonitorNodeRegistry } from \"./BizMonitorNodeRegistry\";\nimport { BizNotifyNodeRegistry } from \"./BizNotifyNodeRegistry\";\nimport { BizUploadNodeRegistry } from \"./BizUploadNodeRegistry\";\nimport { BranchBlockNodeRegistry, ConditionNodeRegistry } from \"./ConditionNode\";\nimport { DelayNodeRegistry } from \"./DelayNode\";\nimport { EndNodeRegistry } from \"./EndNode\";\nimport { StartNodeRegistry } from \"./StartNode\";\nimport { CatchBlockNodeRegistry, TryCatchNodeRegistry } from \"./TryCatchNode\";\n\nexport const getAllNodeRegistries = () => {\n  return [\n    StartNodeRegistry,\n    EndNodeRegistry,\n    DelayNodeRegistry,\n    BizApplyNodeRegistry,\n    BizUploadNodeRegistry,\n    BizMonitorNodeRegistry,\n    BizDeployNodeRegistry,\n    BizNotifyNodeRegistry,\n    ConditionNodeRegistry,\n    BranchBlockNodeRegistry,\n    TryCatchNodeRegistry,\n    CatchBlockNodeRegistry,\n  ];\n};\n\nexport type * from \"./typings\";\n"
  },
  {
    "path": "ui/src/components/workflow/designer/nodes/typings.ts",
    "content": "﻿import {\n  type FixedLayoutPluginContext,\n  type FlowNodeEntity,\n  type FlowNodeJSON,\n  type FlowNodeMeta,\n  type FlowNodeRegistry,\n  type FormMeta,\n  type FormRenderProps,\n} from \"@flowgram.ai/fixed-layout-editor\";\n\nimport { WORKFLOW_NODE_TYPES, type WorkflowNode } from \"@/domain/workflow\";\n\nexport enum NodeType {\n  Start = \"start\",\n  End = \"end\",\n  Delay = \"delay\",\n  Condition = \"condition\",\n  BranchBlock = \"branchBlock\",\n  TryCatch = \"tryCatch\",\n  TryBlock = \"tryBlock\",\n  CatchBlock = \"catchBlock\",\n  BizApply = \"bizApply\",\n  BizUpload = \"bizUpload\",\n  BizMonitor = \"bizMonitor\",\n  BizDeploy = \"bizDeploy\",\n  BizNotify = \"bizNotify\",\n}\n\n/* TYPE GUARD, PLEASE DO NOT REMOVE THESE! */\nconsole.assert(NodeType.Start === WORKFLOW_NODE_TYPES.START);\nconsole.assert(NodeType.End === WORKFLOW_NODE_TYPES.END);\nconsole.assert(NodeType.Delay === WORKFLOW_NODE_TYPES.DELAY);\nconsole.assert(NodeType.Condition === WORKFLOW_NODE_TYPES.CONDITION);\nconsole.assert(NodeType.BranchBlock === WORKFLOW_NODE_TYPES.BRANCHBLOCK);\nconsole.assert(NodeType.TryCatch === WORKFLOW_NODE_TYPES.TRYCATCH);\nconsole.assert(NodeType.TryBlock === WORKFLOW_NODE_TYPES.TRYBLOCK);\nconsole.assert(NodeType.CatchBlock === WORKFLOW_NODE_TYPES.CATCHBLOCK);\nconsole.assert(NodeType.BizApply === WORKFLOW_NODE_TYPES.BIZ_APPLY);\nconsole.assert(NodeType.BizUpload === WORKFLOW_NODE_TYPES.BIZ_UPLOAD);\nconsole.assert(NodeType.BizMonitor === WORKFLOW_NODE_TYPES.BIZ_MONITOR);\nconsole.assert(NodeType.BizDeploy === WORKFLOW_NODE_TYPES.BIZ_DEPLOY);\nconsole.assert(NodeType.BizNotify === WORKFLOW_NODE_TYPES.BIZ_NOTIFY);\n\nexport enum NodeKindType {\n  Basis = \"basis\",\n  Business = \"business\",\n  Logic = \"logic\",\n}\n\nexport interface NodeJSON extends FlowNodeJSON {\n  data: WorkflowNode[\"data\"] & {\n    [key: string]: any;\n  };\n}\n\nexport interface DocumentJSON {\n  nodes: NodeJSON[];\n}\n\nexport interface NodeMeta extends FlowNodeMeta {\n  /**\n   * 自定义样式。\n   */\n  style?: React.CSSProperties;\n  /**\n   * 标题文本。\n   */\n  labelText?: React.ReactNode;\n  /**\n   * 图标组件。\n   */\n  icon?: React.ExoticComponent<any> | React.ComponentType<any>;\n  /**\n   * 图标前景色。\n   */\n  iconColor?: string;\n  /**\n   * 图标背景色。\n   */\n  iconBgColor?: string;\n  /**\n   * 是否可点击。通常配合抽屉表单使用。\n   */\n  clickable?: boolean;\n}\n\nexport interface NodeRegistry<V extends NodeJSON[\"data\"] = NodeJSON[\"data\"]> extends FlowNodeRegistry<NodeMeta> {\n  /**\n   * 节点类型分类。\n   */\n  kind?: NodeKindType;\n\n  formMeta?: Omit<FormMeta<V>, \"render\"> & {\n    render: (props: FormRenderProps<V>) => React.ReactElement;\n  };\n\n  /**\n   * 判断是否可以添加一个该类型的节点。\n   * 如果不存在该方法，默认等同于返回值为 true。\n   * @param {FixedLayoutPluginContext} ctx\n   * @param {FlowNodeEntity} from\n   * @returns {Boolean}\n   */\n  canAdd?: (ctx: FixedLayoutPluginContext, from: FlowNodeEntity) => boolean;\n  /**\n   * 判断是否可以删除一个该类型的节点。\n   * 如果不存在该方法，默认等同于返回值为 true。\n   * @param {FixedLayoutPluginContext} ctx\n   * @param {FlowNodeEntity} from\n   * @returns {Boolean}\n   */\n  canDelete?: (ctx: FixedLayoutPluginContext, from: FlowNodeEntity) => boolean;\n  /**\n   * 返回一个新的表示该类型的节点结构。\n   * @param {FixedLayoutPluginContext} ctx\n   * @param {FlowNodeEntity} from\n   * @returns {FlowNodeJSON}\n   */\n  onAdd?: (ctx: FixedLayoutPluginContext, from: FlowNodeEntity) => FlowNodeJSON;\n}\n"
  },
  {
    "path": "ui/src/domain/access.ts",
    "content": "export interface AccessModel extends BaseModel {\n  name: string;\n  provider: string;\n  config?: Record<string, unknown>;\n  reserve?: \"ca\" | \"notif\";\n}\n"
  },
  {
    "path": "ui/src/domain/app.ts",
    "content": "﻿export const APP_VERSION = \"v\" + (__APP_VERSION__ || \"0.0.0-dev\").replace(/^v/, \"\");\n\nexport const APP_REPO_URL = \"https://github.com/certimate-go/certimate\";\n\nexport const APP_DOWNLOAD_URL = APP_REPO_URL + \"/releases\";\n\nexport const APP_DOCUMENT_URL = \"https://docs.certimate.me\";\n"
  },
  {
    "path": "ui/src/domain/certificate.ts",
    "content": "import { type WorkflowModel } from \"./workflow\";\n\nexport interface CertificateModel extends BaseModel {\n  source: string;\n  subjectAltNames: string;\n  serialNumber: string;\n  certificate: string;\n  privateKey: string;\n  issuerOrg: string;\n  keyAlgorithm: string;\n  validityNotBefore: ISO8601String;\n  validityNotAfter: ISO8601String;\n  isRenewed: boolean;\n  isRevoked: boolean;\n  workflowRef: string;\n  expand?: {\n    workflowRef?: Pick<WorkflowModel, \"id\" | \"name\" | \"description\">;\n  };\n}\n\nexport const CERTIFICATE_SOURCES = Object.freeze({\n  REQUEST: \"request\",\n  UPLOAD: \"upload\",\n} as const);\n\nexport type CertificateSourceType = (typeof CERTIFICATE_SOURCES)[keyof typeof CERTIFICATE_SOURCES];\n\nexport const CERTIFICATE_FORMATS = Object.freeze({\n  PEM: \"PEM\",\n  PFX: \"PFX\",\n  JKS: \"JKS\",\n} as const);\n\nexport type CertificateFormatType = (typeof CERTIFICATE_FORMATS)[keyof typeof CERTIFICATE_FORMATS];\n"
  },
  {
    "path": "ui/src/domain/provider.ts",
    "content": "interface BaseProvider<P> {\r\n  type: P;\r\n  name: string;\r\n  icon: string;\r\n  builtin: boolean;\r\n}\r\n\r\ninterface BaseProviderWithAccess<P> extends BaseProvider<P> {\r\n  provider: AccessProviderType;\r\n}\r\n\r\n// #region AccessProvider\r\n/*\r\n  注意：如果追加新的常量值，请保持以 ASCII 排序。\r\n  NOTICE: If you add new constant, please keep ASCII order.\r\n */\r\nexport const ACCESS_PROVIDERS = Object.freeze({\r\n  [\"1PANEL\"]: \"1panel\",\r\n  [\"35CN\"]: \"35cn\",\r\n  [\"51DNSCOM\"]: \"51dnscom\",\r\n  ACMECA: \"acmeca\",\r\n  ACMEDNS: \"acmedns\",\r\n  ACMEHTTPREQ: \"acmehttpreq\",\r\n  ACTALISSSL: \"actalisssl\",\r\n  AKAMAI: \"akamai\",\r\n  ALIYUN: \"aliyun\",\r\n  APISIX: \"apisix\",\r\n  ARVANCLOUD: \"arvancloud\",\r\n  AWS: \"aws\",\r\n  AZURE: \"azure\",\r\n  BAIDUCLOUD: \"baiducloud\",\r\n  BAISHAN: \"baishan\",\r\n  BAOTAPANEL: \"baotapanel\",\r\n  BAOTAPANELGO: \"baotapanelgo\",\r\n  BAOTAWAF: \"baotawaf\",\r\n  BOOKMYNAME: \"bookmyname\",\r\n  BUNNY: \"bunny\",\r\n  BYTEPLUS: \"byteplus\",\r\n  CACHEFLY: \"cachefly\",\r\n  CDNFLY: \"cdnfly\",\r\n  CLOUDFLARE: \"cloudflare\",\r\n  CLOUDNS: \"cloudns\",\r\n  CMCCCLOUD: \"cmcccloud\",\r\n  CONSTELLIX: \"constellix\",\r\n  CPANEL: \"cpanel\",\r\n  CTCCCLOUD: \"ctcccloud\",\r\n  DESEC: \"desec\",\r\n  DIGICERT: \"digicert\",\r\n  DIGITALOCEAN: \"digitalocean\",\r\n  DINGTALKBOT: \"dingtalkbot\",\r\n  DISCORDBOT: \"discordbot\",\r\n  DNSEXIT: \"dnsexit\",\r\n  DNSLA: \"dnsla\",\r\n  DNSMADEEASY: \"dnsmadeeasy\",\r\n  DOGECLOUD: \"dogecloud\",\r\n  DOKPLOY: \"dokploy\",\r\n  DUCKDNS: \"duckdns\",\r\n  DYNU: \"dynu\",\r\n  DYNV6: \"dynv6\",\r\n  EMAIL: \"email\",\r\n  FLEXCDN: \"flexcdn\",\r\n  FLYIO: \"flyio\",\r\n  GANDINET: \"gandinet\",\r\n  GCORE: \"gcore\",\r\n  GLOBALSIGNATLAS: \"globalsignatlas\",\r\n  GNAME: \"gname\",\r\n  GODADDY: \"godaddy\",\r\n  GOEDGE: \"goedge\",\r\n  GOOGLETRUSTSERVICES: \"googletrustservices\",\r\n  HETZNER: \"hetzner\",\r\n  HOSTINGDE: \"hostingde\",\r\n  HOSTINGER: \"hostinger\",\r\n  HUAWEICLOUD: \"huaweicloud\",\r\n  INFOMANIAK: \"infomaniak\",\r\n  IONOS: \"ionos\",\r\n  JDCLOUD: \"jdcloud\",\r\n  KONG: \"kong\",\r\n  KUBERNETES: \"k8s\",\r\n  KSYUN: \"ksyun\",\r\n  LARKBOT: \"larkbot\",\r\n  LECDN: \"lecdn\",\r\n  LETSENCRYPT: \"letsencrypt\",\r\n  LETSENCRYPTSTAGING: \"letsencryptstaging\",\r\n  LINODE: \"linode\",\r\n  LITESSL: \"litessl\",\r\n  LOCAL: \"local\",\r\n  MATTERMOST: \"mattermost\",\r\n  MOHUA: \"mohua\",\r\n  NAMECHEAP: \"namecheap\",\r\n  NAMEDOTCOM: \"namedotcom\",\r\n  NAMESILO: \"namesilo\",\r\n  NETCUP: \"netcup\",\r\n  NETLIFY: \"netlify\",\r\n  NGINXPROXYMANAGER: \"nginxproxymanager\",\r\n  NS1: \"ns1\",\r\n  OVHCLOUD: \"ovhcloud\",\r\n  PORKBUN: \"porkbun\",\r\n  POWERDNS: \"powerdns\",\r\n  PROXMOXVE: \"proxmoxve\",\r\n  QINGCLOUD: \"qingcloud\",\r\n  QINIU: \"qiniu\",\r\n  RAINYUN: \"rainyun\",\r\n  RATPANEL: \"ratpanel\",\r\n  RFC2136: \"rfc2136\",\r\n  S3: \"s3\",\r\n  SAFELINE: \"safeline\",\r\n  SECTIGO: \"sectigo\",\r\n  SLACKBOT: \"slackbot\",\r\n  SPACESHIP: \"spaceship\",\r\n  SSH: \"ssh\",\r\n  SSLCOM: \"sslcom\",\r\n  SYNOLOGYDSM: \"synologydsm\",\r\n  TECHNITIUMDNS: \"technitiumdns\",\r\n  TELEGRAMBOT: \"telegrambot\",\r\n  TENCENTCLOUD: \"tencentcloud\",\r\n  TODAYNIC: \"todaynic\",\r\n  UCLOUD: \"ucloud\",\r\n  UNICLOUD: \"unicloud\",\r\n  UPYUN: \"upyun\",\r\n  VERCEL: \"vercel\",\r\n  VOLCENGINE: \"volcengine\",\r\n  VULTR: \"vultr\",\r\n  WANGSU: \"wangsu\",\r\n  WEBHOOK: \"webhook\",\r\n  WECOMBOT: \"wecombot\",\r\n  WESTCN: \"westcn\",\r\n  XINNET: \"xinnet\",\r\n  ZEROSSL: \"zerossl\",\r\n} as const);\r\n\r\nexport type AccessProviderType = (typeof ACCESS_PROVIDERS)[keyof typeof ACCESS_PROVIDERS];\r\n\r\nexport const ACCESS_USAGES = Object.freeze({\r\n  DNS: \"dns\",\r\n  HOSTING: \"hosting\",\r\n  CA: \"ca\",\r\n  NOTIFICATION: \"notification\",\r\n} as const);\r\n\r\nexport type AccessUsageType = (typeof ACCESS_USAGES)[keyof typeof ACCESS_USAGES];\r\n\r\nexport interface AccessProvider extends BaseProvider<AccessProviderType> {\r\n  usages: AccessUsageType[];\r\n}\r\n\r\nexport const accessProvidersMap: Map<AccessProvider[\"type\"] | string, AccessProvider> = new Map(\r\n  /*\r\n    注意：此处的顺序决定显示在前端的顺序。\r\n    NOTICE: The following order determines the order displayed at the frontend.\r\n  */\r\n  (\r\n    [\r\n      [ACCESS_PROVIDERS.LOCAL, \"provider.local\", \"/imgs/providers/local.svg\", [ACCESS_USAGES.HOSTING], \"builtin\"],\r\n      [ACCESS_PROVIDERS.SSH, \"provider.ssh\", \"/imgs/providers/ssh.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.WEBHOOK, \"provider.webhook\", \"/imgs/providers/webhook.svg\", [ACCESS_USAGES.HOSTING, ACCESS_USAGES.NOTIFICATION]],\r\n      [ACCESS_PROVIDERS.KUBERNETES, \"provider.kubernetes\", \"/imgs/providers/kubernetes.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.S3, \"provider.s3\", \"/imgs/providers/s3.svg\", [ACCESS_USAGES.HOSTING]],\r\n\r\n      [ACCESS_PROVIDERS.ALIYUN, \"provider.aliyun\", \"/imgs/providers/aliyun.svg\", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.TENCENTCLOUD, \"provider.tencentcloud\", \"/imgs/providers/tencentcloud.svg\", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.BAIDUCLOUD, \"provider.baiducloud\", \"/imgs/providers/baiducloud.svg\", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.HUAWEICLOUD, \"provider.huaweicloud\", \"/imgs/providers/huaweicloud.svg\", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.VOLCENGINE, \"provider.volcengine\", \"/imgs/providers/volcengine.svg\", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.JDCLOUD, \"provider.jdcloud\", \"/imgs/providers/jdcloud.svg\", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.AWS, \"provider.aws\", \"/imgs/providers/aws.svg\", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.AZURE, \"provider.azure\", \"/imgs/providers/azure.svg\", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.BUNNY, \"provider.bunny\", \"/imgs/providers/bunny.svg\", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.GCORE, \"provider.gcore\", \"/imgs/providers/gcore.png\", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.NETLIFY, \"provider.netlify\", \"/imgs/providers/netlify.png\", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.RAINYUN, \"provider.rainyun\", \"/imgs/providers/rainyun.svg\", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.UCLOUD, \"provider.ucloud\", \"/imgs/providers/ucloud.svg\", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.CTCCCLOUD, \"provider.ctcccloud\", \"/imgs/providers/ctcccloud.svg\", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.CPANEL, \"provider.cpanel\", \"/imgs/providers/cpanel.svg\", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],\r\n\r\n      [ACCESS_PROVIDERS.QINIU, \"provider.qiniu\", \"/imgs/providers/qiniu.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.UPYUN, \"provider.upyun\", \"/imgs/providers/upyun.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.BAISHAN, \"provider.baishan\", \"/imgs/providers/baishan.png\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.WANGSU, \"provider.wangsu\", \"/imgs/providers/wangsu.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.DOGECLOUD, \"provider.dogecloud\", \"/imgs/providers/dogecloud.png\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.KSYUN, \"provider.ksyun\", \"/imgs/providers/ksyun.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.BYTEPLUS, \"provider.byteplus\", \"/imgs/providers/byteplus.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.CACHEFLY, \"provider.cachefly\", \"/imgs/providers/cachefly.png\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.MOHUA, \"provider.mohua\", \"/imgs/providers/mohua.png\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.UNICLOUD, \"provider.unicloud\", \"/imgs/providers/unicloud.png\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS[\"1PANEL\"], \"provider.1panel\", \"/imgs/providers/1panel.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.APISIX, \"provider.apisix\", \"/imgs/providers/apisix.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.BAOTAPANEL, \"provider.baotapanel\", \"/imgs/providers/baotapanel.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.BAOTAPANELGO, \"provider.baotapanelgo\", \"/imgs/providers/baotapanel.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.BAOTAWAF, \"provider.baotawaf\", \"/imgs/providers/baotawaf.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.CDNFLY, \"provider.cdnfly\", \"/imgs/providers/cdnfly.png\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.DOKPLOY, \"provider.dokploy\", \"/imgs/providers/dokploy.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.FLEXCDN, \"provider.flexcdn\", \"/imgs/providers/flexcdn.png\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.FLYIO, \"provider.flyio\", \"/imgs/providers/flyio.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.GOEDGE, \"provider.goedge\", \"/imgs/providers/goedge.png\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.KONG, \"provider.kong\", \"/imgs/providers/kong.png\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.LECDN, \"provider.lecdn\", \"/imgs/providers/lecdn.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.NGINXPROXYMANAGER, \"provider.nginxproxymanager\", \"/imgs/providers/nginxproxymanager.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.PROXMOXVE, \"provider.proxmoxve\", \"/imgs/providers/proxmoxve.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.RATPANEL, \"provider.ratpanel\", \"/imgs/providers/ratpanel.png\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.SAFELINE, \"provider.safeline\", \"/imgs/providers/safeline.svg\", [ACCESS_USAGES.HOSTING]],\r\n      [ACCESS_PROVIDERS.SYNOLOGYDSM, \"provider.synologydsm\", \"/imgs/providers/synologydsm.png\", [ACCESS_USAGES.HOSTING]],\r\n\r\n      [ACCESS_PROVIDERS.AKAMAI, \"provider.akamai\", \"/imgs/providers/akamai.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.ARVANCLOUD, \"provider.arvancloud\", \"/imgs/providers/arvancloud.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.BOOKMYNAME, \"provider.bookmyname\", \"/imgs/providers/bookmyname.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.CLOUDFLARE, \"provider.cloudflare\", \"/imgs/providers/cloudflare.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.CLOUDNS, \"provider.cloudns\", \"/imgs/providers/cloudns.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.CONSTELLIX, \"provider.constellix\", \"/imgs/providers/constellix.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.DESEC, \"provider.desec\", \"/imgs/providers/desec.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.DIGITALOCEAN, \"provider.digitalocean\", \"/imgs/providers/digitalocean.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.DNSEXIT, \"provider.dnsexit\", \"/imgs/providers/dnsexit.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.DNSMADEEASY, \"provider.dnsmadeeasy\", \"/imgs/providers/dnsmadeeasy.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.DUCKDNS, \"provider.duckdns\", \"/imgs/providers/duckdns.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.DYNU, \"provider.dynu\", \"/imgs/providers/dynu.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.DYNV6, \"provider.dynv6\", \"/imgs/providers/dynv6.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.GANDINET, \"provider.gandinet\", \"/imgs/providers/gandinet.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.GNAME, \"provider.gname\", \"/imgs/providers/gname.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.GODADDY, \"provider.godaddy\", \"/imgs/providers/godaddy.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.HETZNER, \"provider.hetzner\", \"/imgs/providers/hetzner.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.HOSTINGDE, \"provider.hostingde\", \"/imgs/providers/hostingde.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.HOSTINGER, \"provider.hostinger\", \"/imgs/providers/hostinger.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.INFOMANIAK, \"provider.infomaniak\", \"/imgs/providers/infomaniak.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.IONOS, \"provider.ionos\", \"/imgs/providers/ionos.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.LINODE, \"provider.linode\", \"/imgs/providers/linode.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.NAMECHEAP, \"provider.namecheap\", \"/imgs/providers/namecheap.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.NAMEDOTCOM, \"provider.namedotcom\", \"/imgs/providers/namedotcom.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.NAMESILO, \"provider.namesilo\", \"/imgs/providers/namesilo.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.NETCUP, \"provider.netcup\", \"/imgs/providers/netcup.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.NS1, \"provider.ns1\", \"/imgs/providers/ns1.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.OVHCLOUD, \"provider.ovhcloud\", \"/imgs/providers/ovhcloud.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.PORKBUN, \"provider.porkbun\", \"/imgs/providers/porkbun.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.SPACESHIP, \"provider.spaceship\", \"/imgs/providers/spaceship.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.VERCEL, \"provider.vercel\", \"/imgs/providers/vercel.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.VULTR, \"provider.vultr\", \"/imgs/providers/vultr.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.CMCCCLOUD, \"provider.cmcccloud\", \"/imgs/providers/cmcccloud.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.QINGCLOUD, \"provider.qingcloud\", \"/imgs/providers/qingcloud.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS[\"35CN\"], \"provider.35cn\", \"/imgs/providers/35cn.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS[\"51DNSCOM\"], \"provider.51dnscom\", \"/imgs/providers/51dnscom.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.DNSLA, \"provider.dnsla\", \"/imgs/providers/dnsla.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.TODAYNIC, \"provider.todaynic\", \"/imgs/providers/todaynic.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.WESTCN, \"provider.westcn\", \"/imgs/providers/westcn.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.XINNET, \"provider.xinnet\", \"/imgs/providers/xinnet.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.POWERDNS, \"provider.powerdns\", \"/imgs/providers/powerdns.svg\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.TECHNITIUMDNS, \"provider.technitiumdns\", \"/imgs/providers/technitiumdns.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.RFC2136, \"provider.rfc2136\", \"/imgs/providers/rfc.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.ACMEDNS, \"provider.acmedns\", \"/imgs/providers/acmedns.png\", [ACCESS_USAGES.DNS]],\r\n      [ACCESS_PROVIDERS.ACMEHTTPREQ, \"provider.acmehttpreq\", \"/imgs/providers/acmehttpreq.svg\", [ACCESS_USAGES.DNS]],\r\n\r\n      [ACCESS_PROVIDERS.LETSENCRYPT, \"provider.letsencrypt\", \"/imgs/providers/letsencrypt.svg\", [ACCESS_USAGES.CA], \"builtin\"],\r\n      [ACCESS_PROVIDERS.LETSENCRYPTSTAGING, \"provider.letsencryptstaging\", \"/imgs/providers/letsencrypt.svg\", [ACCESS_USAGES.CA], \"builtin\"],\r\n      [ACCESS_PROVIDERS.ACTALISSSL, \"provider.actalisssl\", \"/imgs/providers/actalisssl.png\", [ACCESS_USAGES.CA]],\r\n      [ACCESS_PROVIDERS.DIGICERT, \"provider.digicert\", \"/imgs/providers/digicert.png\", [ACCESS_USAGES.CA]],\r\n      [ACCESS_PROVIDERS.GLOBALSIGNATLAS, \"provider.globalsignatlas\", \"/imgs/providers/globalsignatlas.png\", [ACCESS_USAGES.CA]],\r\n      [ACCESS_PROVIDERS.GOOGLETRUSTSERVICES, \"provider.googletrustservices\", \"/imgs/providers/google.svg\", [ACCESS_USAGES.CA]],\r\n      [ACCESS_PROVIDERS.LITESSL, \"provider.litessl\", \"/imgs/providers/litessl.svg\", [ACCESS_USAGES.CA]],\r\n      [ACCESS_PROVIDERS.SECTIGO, \"provider.sectigo\", \"/imgs/providers/sectigo.svg\", [ACCESS_USAGES.CA]],\r\n      [ACCESS_PROVIDERS.SSLCOM, \"provider.sslcom\", \"/imgs/providers/sslcom.svg\", [ACCESS_USAGES.CA]],\r\n      [ACCESS_PROVIDERS.ZEROSSL, \"provider.zerossl\", \"/imgs/providers/zerossl.svg\", [ACCESS_USAGES.CA]],\r\n      [ACCESS_PROVIDERS.ACMECA, \"provider.acmeca\", \"/imgs/providers/acmeca.svg\", [ACCESS_USAGES.CA]],\r\n\r\n      [ACCESS_PROVIDERS.EMAIL, \"provider.email\", \"/imgs/providers/email.svg\", [ACCESS_USAGES.NOTIFICATION]],\r\n      [ACCESS_PROVIDERS.DINGTALKBOT, \"provider.dingtalkbot\", \"/imgs/providers/dingtalk.svg\", [ACCESS_USAGES.NOTIFICATION]],\r\n      [ACCESS_PROVIDERS.LARKBOT, \"provider.larkbot\", \"/imgs/providers/lark.svg\", [ACCESS_USAGES.NOTIFICATION]],\r\n      [ACCESS_PROVIDERS.WECOMBOT, \"provider.wecombot\", \"/imgs/providers/wecom.svg\", [ACCESS_USAGES.NOTIFICATION]],\r\n      [ACCESS_PROVIDERS.DISCORDBOT, \"provider.discordbot\", \"/imgs/providers/discord.svg\", [ACCESS_USAGES.NOTIFICATION]],\r\n      [ACCESS_PROVIDERS.SLACKBOT, \"provider.slackbot\", \"/imgs/providers/slack.svg\", [ACCESS_USAGES.NOTIFICATION]],\r\n      [ACCESS_PROVIDERS.TELEGRAMBOT, \"provider.telegrambot\", \"/imgs/providers/telegram.svg\", [ACCESS_USAGES.NOTIFICATION]],\r\n      [ACCESS_PROVIDERS.MATTERMOST, \"provider.mattermost\", \"/imgs/providers/mattermost.svg\", [ACCESS_USAGES.NOTIFICATION]],\r\n    ] satisfies Array<[AccessProviderType, string, string, AccessUsageType[], \"builtin\"] | [AccessProviderType, string, string, AccessUsageType[]]>\r\n  ).map(([type, name, icon, usages, builtin]) => [\r\n    type,\r\n    {\r\n      type: type,\r\n      name: name,\r\n      icon: icon,\r\n      usages: usages,\r\n      builtin: builtin === \"builtin\",\r\n    },\r\n  ])\r\n);\r\n// #endregion\r\n\r\n// #region CAProvider\r\n/*\r\n  注意：如果追加新的常量值，请保持以 ASCII 排序。\r\n  NOTICE: If you add new constant, please keep ASCII order.\r\n */\r\nexport const CA_PROVIDERS = Object.freeze({\r\n  ACMECA: `${ACCESS_PROVIDERS.ACMECA}`,\r\n  ACTALISSSL: `${ACCESS_PROVIDERS.ACTALISSSL}`,\r\n  GLOBALSIGNATLAS: `${ACCESS_PROVIDERS.GLOBALSIGNATLAS}`,\r\n  GOOGLETRUSTSERVICES: `${ACCESS_PROVIDERS.GOOGLETRUSTSERVICES}`,\r\n  LETSENCRYPT: `${ACCESS_PROVIDERS.LETSENCRYPT}`,\r\n  LETSENCRYPTSTAGING: `${ACCESS_PROVIDERS.LETSENCRYPTSTAGING}`,\r\n  LITESSL: `${ACCESS_PROVIDERS.LITESSL}`,\r\n  SECTIGO: `${ACCESS_PROVIDERS.SECTIGO}`,\r\n  SSLCOM: `${ACCESS_PROVIDERS.SSLCOM}`,\r\n  ZEROSSL: `${ACCESS_PROVIDERS.ZEROSSL}`,\r\n} as const);\r\n\r\nexport type CAProviderType = (typeof CA_PROVIDERS)[keyof typeof CA_PROVIDERS];\r\n\r\nexport interface CAProvider extends BaseProviderWithAccess<CAProviderType> {}\r\n\r\nexport const caProvidersMap: Map<CAProvider[\"type\"] | string, CAProvider> = new Map(\r\n  /*\r\n    注意：此处的顺序决定显示在前端的顺序。\r\n    NOTICE: The following order determines the order displayed at the frontend.\r\n  */\r\n  (\r\n    [\r\n      [CA_PROVIDERS.LETSENCRYPT, \"builtin\"],\r\n      [CA_PROVIDERS.LETSENCRYPTSTAGING, \"builtin\"],\r\n      [CA_PROVIDERS.ACTALISSSL],\r\n      [CA_PROVIDERS.GLOBALSIGNATLAS],\r\n      [CA_PROVIDERS.GOOGLETRUSTSERVICES],\r\n      [CA_PROVIDERS.SECTIGO],\r\n      [CA_PROVIDERS.SSLCOM],\r\n      [CA_PROVIDERS.ZEROSSL],\r\n      [CA_PROVIDERS.LITESSL],\r\n      [CA_PROVIDERS.ACMECA],\r\n    ] satisfies Array<[CAProviderType, \"builtin\"] | [CAProviderType]>\r\n  ).map(([type, builtin]) => [\r\n    type,\r\n    {\r\n      type: type,\r\n      name: accessProvidersMap.get(type.split(\"-\")[0])!.name,\r\n      icon: accessProvidersMap.get(type.split(\"-\")[0])!.icon,\r\n      provider: type.split(\"-\")[0] as AccessProviderType,\r\n      builtin: builtin === \"builtin\",\r\n    },\r\n  ])\r\n);\r\n// #endregion\r\n\r\n// #region ACMEDNS01Provider\r\n/*\r\n  注意：如果追加新的常量值，请保持以 ASCII 排序。\r\n  NOTICE: If you add new constant, please keep ASCII order.\r\n */\r\nexport const ACME_DNS01_PROVIDERS = Object.freeze({\r\n  [\"35CN\"]: `${ACCESS_PROVIDERS[\"35CN\"]}`,\r\n  [\"51DNSCOM\"]: `${ACCESS_PROVIDERS[\"51DNSCOM\"]}`,\r\n  ACMEDNS: `${ACCESS_PROVIDERS.ACMEDNS}`,\r\n  ACMEHTTPREQ: `${ACCESS_PROVIDERS.ACMEHTTPREQ}`,\r\n  AKAMAI: `${ACCESS_PROVIDERS.AKAMAI}`, // 兼容旧值，等同于 `AKAMAI_EDGEDNS`\r\n  AKAMAI_EDGEDNS: `${ACCESS_PROVIDERS.AKAMAI}-edgedns`,\r\n  ALIYUN: `${ACCESS_PROVIDERS.ALIYUN}`, // 兼容旧值，等同于 `ALIYUN_DNS`\r\n  ALIYUN_DNS: `${ACCESS_PROVIDERS.ALIYUN}-dns`,\r\n  ALIYUN_ESA: `${ACCESS_PROVIDERS.ALIYUN}-esa`,\r\n  ARVANCLOUD: `${ACCESS_PROVIDERS.ARVANCLOUD}`,\r\n  AWS: `${ACCESS_PROVIDERS.AWS}`, // 兼容旧值，等同于 `AWS_ROUTE53`\r\n  AWS_ROUTE53: `${ACCESS_PROVIDERS.AWS}-route53`,\r\n  AZURE: `${ACCESS_PROVIDERS.AZURE}`, // 兼容旧值，等同于 `AZURE_DNS`\r\n  AZURE_DNS: `${ACCESS_PROVIDERS.AZURE}-dns`,\r\n  BAIDUCLOUD: `${ACCESS_PROVIDERS.BAIDUCLOUD}`, // 兼容旧值，等同于 `BAIDUCLOUD_DNS`\r\n  BAIDUCLOUD_DNS: `${ACCESS_PROVIDERS.BAIDUCLOUD}-dns`,\r\n  BOOKMYNAME: `${ACCESS_PROVIDERS.BOOKMYNAME}`,\r\n  BUNNY: `${ACCESS_PROVIDERS.BUNNY}`,\r\n  CLOUDFLARE: `${ACCESS_PROVIDERS.CLOUDFLARE}`,\r\n  CLOUDNS: `${ACCESS_PROVIDERS.CLOUDNS}`,\r\n  CMCCCLOUD: `${ACCESS_PROVIDERS.CMCCCLOUD}`, // 兼容旧值，等同于 `CMCCCLOUD_DNS`\r\n  CMCCCLOUD_DNS: `${ACCESS_PROVIDERS.CMCCCLOUD}-dns`,\r\n  CONSTELLIX: `${ACCESS_PROVIDERS.CONSTELLIX}`,\r\n  CPANEL: `${ACCESS_PROVIDERS.CPANEL}`,\r\n  CTCCCLOUD: `${ACCESS_PROVIDERS.CTCCCLOUD}`, // 兼容旧值，等同于 `CTCCCLOUD_SMARTDNS`\r\n  CTCCCLOUD_SMARTDNS: `${ACCESS_PROVIDERS.CTCCCLOUD}-smartdns`,\r\n  DESEC: `${ACCESS_PROVIDERS.DESEC}`,\r\n  DIGITALOCEAN: `${ACCESS_PROVIDERS.DIGITALOCEAN}`,\r\n  DNSEXIT: `${ACCESS_PROVIDERS.DNSEXIT}`,\r\n  DNSLA: `${ACCESS_PROVIDERS.DNSLA}`,\r\n  DNSMADEEASY: `${ACCESS_PROVIDERS.DNSMADEEASY}`,\r\n  DUCKDNS: `${ACCESS_PROVIDERS.DUCKDNS}`,\r\n  DYNU: `${ACCESS_PROVIDERS.DYNU}`,\r\n  DYNV6: `${ACCESS_PROVIDERS.DYNV6}`,\r\n  GANDINET: `${ACCESS_PROVIDERS.GANDINET}`,\r\n  GCORE: `${ACCESS_PROVIDERS.GCORE}`,\r\n  GNAME: `${ACCESS_PROVIDERS.GNAME}`,\r\n  GODADDY: `${ACCESS_PROVIDERS.GODADDY}`,\r\n  HETZNER: `${ACCESS_PROVIDERS.HETZNER}`,\r\n  HOSTINGDE: `${ACCESS_PROVIDERS.HOSTINGDE}`,\r\n  HOSTINGER: `${ACCESS_PROVIDERS.HOSTINGER}`,\r\n  HUAWEICLOUD: `${ACCESS_PROVIDERS.HUAWEICLOUD}`, // 兼容旧值，等同于 `HUAWEICLOUD_DNS`\r\n  HUAWEICLOUD_DNS: `${ACCESS_PROVIDERS.HUAWEICLOUD}-dns`,\r\n  INFOMANIAK: `${ACCESS_PROVIDERS.INFOMANIAK}`,\r\n  IONOS: `${ACCESS_PROVIDERS.IONOS}`,\r\n  JDCLOUD: `${ACCESS_PROVIDERS.JDCLOUD}`, // 兼容旧值，等同于 `JDCLOUD_DNS`\r\n  JDCLOUD_DNS: `${ACCESS_PROVIDERS.JDCLOUD}-dns`,\r\n  LINODE: `${ACCESS_PROVIDERS.LINODE}`,\r\n  NAMECHEAP: `${ACCESS_PROVIDERS.NAMECHEAP}`,\r\n  NAMEDOTCOM: `${ACCESS_PROVIDERS.NAMEDOTCOM}`,\r\n  NAMESILO: `${ACCESS_PROVIDERS.NAMESILO}`,\r\n  NETCUP: `${ACCESS_PROVIDERS.NETCUP}`,\r\n  NETLIFY: `${ACCESS_PROVIDERS.NETLIFY}`,\r\n  NS1: `${ACCESS_PROVIDERS.NS1}`,\r\n  OVHCLOUD: `${ACCESS_PROVIDERS.OVHCLOUD}`,\r\n  PORKBUN: `${ACCESS_PROVIDERS.PORKBUN}`,\r\n  POWERDNS: `${ACCESS_PROVIDERS.POWERDNS}`,\r\n  QINGCLOUD: `${ACCESS_PROVIDERS.QINGCLOUD}`, // 兼容旧值，等同于 `QINGCLOUD_DNS`\r\n  QINGCLOUD_DNS: `${ACCESS_PROVIDERS.QINGCLOUD}-dns`,\r\n  RAINYUN: `${ACCESS_PROVIDERS.RAINYUN}`,\r\n  RFC2136: `${ACCESS_PROVIDERS.RFC2136}`,\r\n  SPACESHIP: `${ACCESS_PROVIDERS.SPACESHIP}`,\r\n  UCLOUD: `${ACCESS_PROVIDERS.UCLOUD}`, // 兼容旧值，等同于 `UCLOUD_UDNR`\r\n  UCLOUD_UDNR: `${ACCESS_PROVIDERS.UCLOUD}-udnr`,\r\n  TECHNITIUMDNS: `${ACCESS_PROVIDERS.TECHNITIUMDNS}`,\r\n  TENCENTCLOUD: `${ACCESS_PROVIDERS.TENCENTCLOUD}`, // 兼容旧值，等同于 `TENCENTCLOUD_DNS`\r\n  TENCENTCLOUD_DNS: `${ACCESS_PROVIDERS.TENCENTCLOUD}-dns`,\r\n  TENCENTCLOUD_EO: `${ACCESS_PROVIDERS.TENCENTCLOUD}-eo`,\r\n  TODAYNIC: `${ACCESS_PROVIDERS.TODAYNIC}`,\r\n  VERCEL: `${ACCESS_PROVIDERS.VERCEL}`,\r\n  VOLCENGINE: `${ACCESS_PROVIDERS.VOLCENGINE}`, // 兼容旧值，等同于 `VOLCENGINE_DNS`\r\n  VOLCENGINE_DNS: `${ACCESS_PROVIDERS.VOLCENGINE}-dns`,\r\n  VULTR: `${ACCESS_PROVIDERS.VULTR}`,\r\n  WESTCN: `${ACCESS_PROVIDERS.WESTCN}`,\r\n  XINNET: `${ACCESS_PROVIDERS.XINNET}`,\r\n} as const);\r\n\r\nexport type ACMEDns01ProviderType = (typeof ACME_DNS01_PROVIDERS)[keyof typeof ACME_DNS01_PROVIDERS];\r\n\r\nexport interface ACMEDns01Provider extends BaseProviderWithAccess<ACMEDns01ProviderType> {}\r\n\r\nexport const acmeDns01ProvidersMap: Map<ACMEDns01Provider[\"type\"] | string, ACMEDns01Provider> = new Map(\r\n  /*\r\n    注意：此处的顺序决定显示在前端的顺序。\r\n    NOTICE: The following order determines the order displayed at the frontend.\r\n   */\r\n  (\r\n    [\r\n      [ACME_DNS01_PROVIDERS.ALIYUN_DNS, \"provider.aliyun_dns\"],\r\n      [ACME_DNS01_PROVIDERS.ALIYUN_ESA, \"provider.aliyun_esa\"],\r\n      [ACME_DNS01_PROVIDERS.TENCENTCLOUD_DNS, \"provider.tencentcloud_dns\"],\r\n      [ACME_DNS01_PROVIDERS.TENCENTCLOUD_EO, \"provider.tencentcloud_eo\"],\r\n      [ACME_DNS01_PROVIDERS.BAIDUCLOUD_DNS, \"provider.baiducloud_dns\"],\r\n      [ACME_DNS01_PROVIDERS.HUAWEICLOUD_DNS, \"provider.huaweicloud_dns\"],\r\n      [ACME_DNS01_PROVIDERS.VOLCENGINE_DNS, \"provider.volcengine_dns\"],\r\n      [ACME_DNS01_PROVIDERS.JDCLOUD_DNS, \"provider.jdcloud_dns\"],\r\n      [ACME_DNS01_PROVIDERS.AWS_ROUTE53, \"provider.aws_route53\"],\r\n      [ACME_DNS01_PROVIDERS.AZURE_DNS, \"provider.azure_dns\"],\r\n      [ACME_DNS01_PROVIDERS.AKAMAI_EDGEDNS, \"provider.akamai_edgedns\"],\r\n      [ACME_DNS01_PROVIDERS.ARVANCLOUD, \"provider.arvancloud\"],\r\n      [ACME_DNS01_PROVIDERS.BOOKMYNAME, \"provider.bookmyname\"],\r\n      [ACME_DNS01_PROVIDERS.BUNNY, \"provider.bunny\"],\r\n      [ACME_DNS01_PROVIDERS.CLOUDFLARE, \"provider.cloudflare\"],\r\n      [ACME_DNS01_PROVIDERS.CLOUDNS, \"provider.cloudns\"],\r\n      [ACME_DNS01_PROVIDERS.CONSTELLIX, \"provider.constellix\"],\r\n      [ACME_DNS01_PROVIDERS.DESEC, \"provider.desec\"],\r\n      [ACME_DNS01_PROVIDERS.DIGITALOCEAN, \"provider.digitalocean\"],\r\n      [ACME_DNS01_PROVIDERS.DNSEXIT, \"provider.dnsexit\"],\r\n      [ACME_DNS01_PROVIDERS.DNSMADEEASY, \"provider.dnsmadeeasy\"],\r\n      [ACME_DNS01_PROVIDERS.DUCKDNS, \"provider.duckdns\"],\r\n      [ACME_DNS01_PROVIDERS.DYNU, \"provider.dynu\"],\r\n      [ACME_DNS01_PROVIDERS.DYNV6, \"provider.dynv6\"],\r\n      [ACME_DNS01_PROVIDERS.GANDINET, \"provider.gandinet\"],\r\n      [ACME_DNS01_PROVIDERS.GCORE, \"provider.gcore\"],\r\n      [ACME_DNS01_PROVIDERS.GNAME, \"provider.gname\"],\r\n      [ACME_DNS01_PROVIDERS.GODADDY, \"provider.godaddy\"],\r\n      [ACME_DNS01_PROVIDERS.HETZNER, \"provider.hetzner\"],\r\n      [ACME_DNS01_PROVIDERS.HOSTINGDE, \"provider.hostingde\"],\r\n      [ACME_DNS01_PROVIDERS.HOSTINGER, \"provider.hostinger\"],\r\n      [ACME_DNS01_PROVIDERS.INFOMANIAK, \"provider.infomaniak\"],\r\n      [ACME_DNS01_PROVIDERS.IONOS, \"provider.ionos\"],\r\n      [ACME_DNS01_PROVIDERS.LINODE, \"provider.linode\"],\r\n      [ACME_DNS01_PROVIDERS.NAMECHEAP, \"provider.namecheap\"],\r\n      [ACME_DNS01_PROVIDERS.NAMEDOTCOM, \"provider.namedotcom\"],\r\n      [ACME_DNS01_PROVIDERS.NAMESILO, \"provider.namesilo\"],\r\n      [ACME_DNS01_PROVIDERS.NETCUP, \"provider.netcup\"],\r\n      [ACME_DNS01_PROVIDERS.NETLIFY, \"provider.netlify\"],\r\n      [ACME_DNS01_PROVIDERS.NS1, \"provider.ns1\"],\r\n      [ACME_DNS01_PROVIDERS.OVHCLOUD, \"provider.ovhcloud\"],\r\n      [ACME_DNS01_PROVIDERS.PORKBUN, \"provider.porkbun\"],\r\n      [ACME_DNS01_PROVIDERS.SPACESHIP, \"provider.spaceship\"],\r\n      [ACME_DNS01_PROVIDERS.VERCEL, \"provider.vercel\"],\r\n      [ACME_DNS01_PROVIDERS.VULTR, \"provider.vultr\"],\r\n      [ACME_DNS01_PROVIDERS.CMCCCLOUD_DNS, \"provider.cmcccloud_dns\"],\r\n      [ACME_DNS01_PROVIDERS.CTCCCLOUD_SMARTDNS, \"provider.ctcccloud_smartdns\"],\r\n      [ACME_DNS01_PROVIDERS.RAINYUN, \"provider.rainyun\"],\r\n      [ACME_DNS01_PROVIDERS.UCLOUD_UDNR, \"provider.ucloud_udnr\"],\r\n      [ACME_DNS01_PROVIDERS.QINGCLOUD_DNS, \"provider.qingcloud_dns\"],\r\n      [ACME_DNS01_PROVIDERS[\"35CN\"], \"provider.35cn\"],\r\n      [ACME_DNS01_PROVIDERS[\"51DNSCOM\"], \"provider.51dnscom\"],\r\n      [ACME_DNS01_PROVIDERS.DNSLA, \"provider.dnsla\"],\r\n      [ACME_DNS01_PROVIDERS.TODAYNIC, \"provider.todaynic\"],\r\n      [ACME_DNS01_PROVIDERS.WESTCN, \"provider.westcn\"],\r\n      [ACME_DNS01_PROVIDERS.XINNET, \"provider.xinnet\"],\r\n      [ACME_DNS01_PROVIDERS.CPANEL, \"provider.cpanel\"],\r\n      [ACME_DNS01_PROVIDERS.POWERDNS, \"provider.powerdns\"],\r\n      [ACME_DNS01_PROVIDERS.TECHNITIUMDNS, \"provider.technitiumdns\"],\r\n      [ACME_DNS01_PROVIDERS.ACMEDNS, \"provider.acmedns\"],\r\n      [ACME_DNS01_PROVIDERS.RFC2136, \"provider.rfc2136\"],\r\n      [ACME_DNS01_PROVIDERS.ACMEHTTPREQ, \"provider.acmehttpreq\"],\r\n    ] satisfies Array<[ACMEDns01ProviderType, string]>\r\n  ).map(([type, name]) => [\r\n    type,\r\n    {\r\n      type: type,\r\n      name: name,\r\n      icon: accessProvidersMap.get(type.split(\"-\")[0])!.icon,\r\n      provider: type.split(\"-\")[0] as AccessProviderType,\r\n      builtin: false,\r\n    },\r\n  ])\r\n);\r\n// #endregion\r\n\r\n// #region ACMEHTTP01Provider\r\n/*\r\n  注意：如果追加新的常量值，请保持以 ASCII 排序。\r\n  NOTICE: If you add new constant, please keep ASCII order.\r\n */\r\nexport const ACME_HTTP01_PROVIDERS = Object.freeze({\r\n  LOCAL: `${ACCESS_PROVIDERS.LOCAL}`,\r\n  S3: `${ACCESS_PROVIDERS.S3}`,\r\n  SSH: `${ACCESS_PROVIDERS.SSH}`,\r\n} as const);\r\n\r\nexport type ACMEHttp01ProviderType = (typeof ACME_HTTP01_PROVIDERS)[keyof typeof ACME_HTTP01_PROVIDERS];\r\n\r\nexport interface ACMEHttp01Provider extends BaseProviderWithAccess<ACMEHttp01ProviderType> {}\r\n\r\nexport const acmeHttp01ProvidersMap: Map<ACMEHttp01Provider[\"type\"] | string, ACMEHttp01Provider> = new Map(\r\n  /*\r\n    注意：此处的顺序决定显示在前端的顺序。\r\n    NOTICE: The following order determines the order displayed at the frontend.\r\n   */\r\n  (\r\n    [\r\n      [ACME_HTTP01_PROVIDERS.LOCAL, \"provider.local\", \"builtin\"],\r\n      [ACME_HTTP01_PROVIDERS.SSH, \"provider.ssh\"],\r\n      [ACME_HTTP01_PROVIDERS.S3, \"provider.s3\"],\r\n    ] satisfies Array<[ACMEHttp01ProviderType, string, \"builtin\"] | [ACMEHttp01ProviderType, string]>\r\n  ).map(([type, name, builtin]) => [\r\n    type,\r\n    {\r\n      type: type,\r\n      name: name,\r\n      icon: accessProvidersMap.get(type.split(\"-\")[0])!.icon,\r\n      provider: type.split(\"-\")[0] as AccessProviderType,\r\n      builtin: builtin === \"builtin\",\r\n    },\r\n  ])\r\n);\r\n// #endregion\r\n\r\n// #region DeploymentProvider\r\n/*\r\n  注意：如果追加新的常量值，请保持以 ASCII 排序。\r\n  NOTICE: If you add new constant, please keep ASCII order.\r\n */\r\nexport const DEPLOYMENT_PROVIDERS = Object.freeze({\r\n  [\"1PANEL\"]: `${ACCESS_PROVIDERS[\"1PANEL\"]}`,\r\n  [\"1PANEL_CONSOLE\"]: `${ACCESS_PROVIDERS[\"1PANEL\"]}-console`,\r\n  ALIYUN_ALB: `${ACCESS_PROVIDERS.ALIYUN}-alb`,\r\n  ALIYUN_APIGW: `${ACCESS_PROVIDERS.ALIYUN}-apigw`,\r\n  ALIYUN_CAS: `${ACCESS_PROVIDERS.ALIYUN}-cas`,\r\n  ALIYUN_CAS_DEPLOY: `${ACCESS_PROVIDERS.ALIYUN}-casdeploy`,\r\n  ALIYUN_CDN: `${ACCESS_PROVIDERS.ALIYUN}-cdn`,\r\n  ALIYUN_CLB: `${ACCESS_PROVIDERS.ALIYUN}-clb`,\r\n  ALIYUN_DCDN: `${ACCESS_PROVIDERS.ALIYUN}-dcdn`,\r\n  ALIYUN_DDOSPRO: `${ACCESS_PROVIDERS.ALIYUN}-ddospro`,\r\n  ALIYUN_ESA: `${ACCESS_PROVIDERS.ALIYUN}-esa`,\r\n  ALIYUN_ESA_SAAS: `${ACCESS_PROVIDERS.ALIYUN}-esasaas`,\r\n  ALIYUN_FC: `${ACCESS_PROVIDERS.ALIYUN}-fc`,\r\n  ALIYUN_GA: `${ACCESS_PROVIDERS.ALIYUN}-ga`,\r\n  ALIYUN_LIVE: `${ACCESS_PROVIDERS.ALIYUN}-live`,\r\n  ALIYUN_NLB: `${ACCESS_PROVIDERS.ALIYUN}-nlb`,\r\n  ALIYUN_OSS: `${ACCESS_PROVIDERS.ALIYUN}-oss`,\r\n  ALIYUN_VOD: `${ACCESS_PROVIDERS.ALIYUN}-vod`,\r\n  ALIYUN_WAF: `${ACCESS_PROVIDERS.ALIYUN}-waf`,\r\n  APISIX: `${ACCESS_PROVIDERS.APISIX}`,\r\n  AWS_ACM: `${ACCESS_PROVIDERS.AWS}-acm`,\r\n  AWS_CLOUDFRONT: `${ACCESS_PROVIDERS.AWS}-cloudfront`,\r\n  AWS_IAM: `${ACCESS_PROVIDERS.AWS}-iam`,\r\n  AZURE_KEYVAULT: `${ACCESS_PROVIDERS.AZURE}-keyvault`,\r\n  BAIDUCLOUD_APPBLB: `${ACCESS_PROVIDERS.BAIDUCLOUD}-appblb`,\r\n  BAIDUCLOUD_BLB: `${ACCESS_PROVIDERS.BAIDUCLOUD}-blb`,\r\n  BAIDUCLOUD_CDN: `${ACCESS_PROVIDERS.BAIDUCLOUD}-cdn`,\r\n  BAIDUCLOUD_CERT: `${ACCESS_PROVIDERS.BAIDUCLOUD}-cert`,\r\n  BAISHAN_CDN: `${ACCESS_PROVIDERS.BAISHAN}-cdn`,\r\n  BAOTAPANEL: `${ACCESS_PROVIDERS.BAOTAPANEL}`,\r\n  BAOTAPANEL_CONSOLE: `${ACCESS_PROVIDERS.BAOTAPANEL}-console`,\r\n  BAOTAPANELGO: `${ACCESS_PROVIDERS.BAOTAPANELGO}`,\r\n  BAOTAPANELGO_CONSOLE: `${ACCESS_PROVIDERS.BAOTAPANELGO}-console`,\r\n  BAOTAWAF: `${ACCESS_PROVIDERS.BAOTAWAF}`,\r\n  BAOTAWAF_CONSOLE: `${ACCESS_PROVIDERS.BAOTAWAF}-console`,\r\n  BUNNY_CDN: `${ACCESS_PROVIDERS.BUNNY}-cdn`,\r\n  BYTEPLUS_CDN: `${ACCESS_PROVIDERS.BYTEPLUS}-cdn`,\r\n  CACHEFLY: `${ACCESS_PROVIDERS.CACHEFLY}`,\r\n  CDNFLY: `${ACCESS_PROVIDERS.CDNFLY}`,\r\n  CPANEL: `${ACCESS_PROVIDERS.CPANEL}`,\r\n  CTCCCLOUD_AO: `${ACCESS_PROVIDERS.CTCCCLOUD}-ao`,\r\n  CTCCCLOUD_CDN: `${ACCESS_PROVIDERS.CTCCCLOUD}-cdn`,\r\n  CTCCCLOUD_CMS: `${ACCESS_PROVIDERS.CTCCCLOUD}-cms`,\r\n  CTCCCLOUD_ELB: `${ACCESS_PROVIDERS.CTCCCLOUD}-elb`,\r\n  CTCCCLOUD_FAAS: `${ACCESS_PROVIDERS.CTCCCLOUD}-faas`,\r\n  CTCCCLOUD_ICDN: `${ACCESS_PROVIDERS.CTCCCLOUD}-icdn`,\r\n  CTCCCLOUD_LVDN: `${ACCESS_PROVIDERS.CTCCCLOUD}-lvdn`,\r\n  DOGECLOUD_CDN: `${ACCESS_PROVIDERS.DOGECLOUD}-cdn`,\r\n  DOKPLOY: `${ACCESS_PROVIDERS.DOKPLOY}`,\r\n  FLEXCDN: `${ACCESS_PROVIDERS.FLEXCDN}`,\r\n  FLYIO: `${ACCESS_PROVIDERS.FLYIO}`,\r\n  GCORE_CDN: `${ACCESS_PROVIDERS.GCORE}-cdn`,\r\n  GOEDGE: `${ACCESS_PROVIDERS.GOEDGE}`,\r\n  HUAWEICLOUD_CDN: `${ACCESS_PROVIDERS.HUAWEICLOUD}-cdn`,\r\n  HUAWEICLOUD_ELB: `${ACCESS_PROVIDERS.HUAWEICLOUD}-elb`,\r\n  HUAWEICLOUD_SCM: `${ACCESS_PROVIDERS.HUAWEICLOUD}-scm`,\r\n  HUAWEICLOUD_OBS: `${ACCESS_PROVIDERS.HUAWEICLOUD}-obs`,\r\n  HUAWEICLOUD_WAF: `${ACCESS_PROVIDERS.HUAWEICLOUD}-waf`,\r\n  JDCLOUD_ALB: `${ACCESS_PROVIDERS.JDCLOUD}-alb`,\r\n  JDCLOUD_CDN: `${ACCESS_PROVIDERS.JDCLOUD}-cdn`,\r\n  JDCLOUD_LIVE: `${ACCESS_PROVIDERS.JDCLOUD}-live`,\r\n  JDCLOUD_VOD: `${ACCESS_PROVIDERS.JDCLOUD}-vod`,\r\n  KONG: `${ACCESS_PROVIDERS.KONG}`,\r\n  KUBERNETES_SECRET: `${ACCESS_PROVIDERS.KUBERNETES}-secret`,\r\n  KSYUN_CDN: `${ACCESS_PROVIDERS.KSYUN}-cdn`,\r\n  LECDN: `${ACCESS_PROVIDERS.LECDN}`,\r\n  LOCAL: `${ACCESS_PROVIDERS.LOCAL}`,\r\n  MOHUA_MVH: `${ACCESS_PROVIDERS.MOHUA}-mvh`,\r\n  NETLIFY: `${ACCESS_PROVIDERS.NETLIFY}`,\r\n  NGINXPROXYMANAGER: `${ACCESS_PROVIDERS.NGINXPROXYMANAGER}`,\r\n  PROXMOXVE: `${ACCESS_PROVIDERS.PROXMOXVE}`,\r\n  QINIU_CDN: `${ACCESS_PROVIDERS.QINIU}-cdn`,\r\n  QINIU_KODO: `${ACCESS_PROVIDERS.QINIU}-kodo`,\r\n  QINIU_PILI: `${ACCESS_PROVIDERS.QINIU}-pili`,\r\n  RAINYUN_RCDN: `${ACCESS_PROVIDERS.RAINYUN}-rcdn`,\r\n  RAINYUN_SSLCENTER: `${ACCESS_PROVIDERS.RAINYUN}-sslcenter`,\r\n  RATPANEL: `${ACCESS_PROVIDERS.RATPANEL}`,\r\n  RATPANEL_CONSOLE: `${ACCESS_PROVIDERS.RATPANEL}-console`,\r\n  S3: `${ACCESS_PROVIDERS.S3}`,\r\n  SAFELINE: `${ACCESS_PROVIDERS.SAFELINE}`,\r\n  SSH: `${ACCESS_PROVIDERS.SSH}`,\r\n  SYNOLOGYDSM: `${ACCESS_PROVIDERS.SYNOLOGYDSM}`,\r\n  TENCENTCLOUD_CDN: `${ACCESS_PROVIDERS.TENCENTCLOUD}-cdn`,\r\n  TENCENTCLOUD_CLB: `${ACCESS_PROVIDERS.TENCENTCLOUD}-clb`,\r\n  TENCENTCLOUD_COS: `${ACCESS_PROVIDERS.TENCENTCLOUD}-cos`,\r\n  TENCENTCLOUD_CSS: `${ACCESS_PROVIDERS.TENCENTCLOUD}-css`,\r\n  TENCENTCLOUD_ECDN: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ecdn`,\r\n  TENCENTCLOUD_EO: `${ACCESS_PROVIDERS.TENCENTCLOUD}-eo`,\r\n  TENCENTCLOUD_GAAP: `${ACCESS_PROVIDERS.TENCENTCLOUD}-gaap`,\r\n  TENCENTCLOUD_SCF: `${ACCESS_PROVIDERS.TENCENTCLOUD}-scf`,\r\n  TENCENTCLOUD_SSL: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ssl`,\r\n  TENCENTCLOUD_SSL_DEPLOY: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ssldeploy`,\r\n  TENCENTCLOUD_SSL_UPDATE: `${ACCESS_PROVIDERS.TENCENTCLOUD}-sslupdate`,\r\n  TENCENTCLOUD_VOD: `${ACCESS_PROVIDERS.TENCENTCLOUD}-vod`,\r\n  TENCENTCLOUD_WAF: `${ACCESS_PROVIDERS.TENCENTCLOUD}-waf`,\r\n  UCLOUD_UALB: `${ACCESS_PROVIDERS.UCLOUD}-ualb`,\r\n  UCLOUD_UCDN: `${ACCESS_PROVIDERS.UCLOUD}-ucdn`,\r\n  UCLOUD_UCLB: `${ACCESS_PROVIDERS.UCLOUD}-uclb`,\r\n  UCLOUD_UEWAF: `${ACCESS_PROVIDERS.UCLOUD}-uewaf`,\r\n  UCLOUD_UPATHX: `${ACCESS_PROVIDERS.UCLOUD}-upathx`,\r\n  UCLOUD_US3: `${ACCESS_PROVIDERS.UCLOUD}-us3`,\r\n  UNICLOUD_WEBHOST: `${ACCESS_PROVIDERS.UNICLOUD}-webhost`,\r\n  UPYUN_CDN: `${ACCESS_PROVIDERS.UPYUN}-cdn`,\r\n  UPYUN_FILE: `${ACCESS_PROVIDERS.UPYUN}-file`,\r\n  VOLCENGINE_ALB: `${ACCESS_PROVIDERS.VOLCENGINE}-alb`,\r\n  VOLCENGINE_CDN: `${ACCESS_PROVIDERS.VOLCENGINE}-cdn`,\r\n  VOLCENGINE_CERTCENTER: `${ACCESS_PROVIDERS.VOLCENGINE}-certcenter`,\r\n  VOLCENGINE_CLB: `${ACCESS_PROVIDERS.VOLCENGINE}-clb`,\r\n  VOLCENGINE_DCDN: `${ACCESS_PROVIDERS.VOLCENGINE}-dcdn`,\r\n  VOLCENGINE_IMAGEX: `${ACCESS_PROVIDERS.VOLCENGINE}-imagex`,\r\n  VOLCENGINE_LIVE: `${ACCESS_PROVIDERS.VOLCENGINE}-live`,\r\n  VOLCENGINE_TOS: `${ACCESS_PROVIDERS.VOLCENGINE}-tos`,\r\n  VOLCENGINE_VOD: `${ACCESS_PROVIDERS.VOLCENGINE}-vod`,\r\n  VOLCENGINE_WAF: `${ACCESS_PROVIDERS.VOLCENGINE}-waf`,\r\n  WANGSU_CDN: `${ACCESS_PROVIDERS.WANGSU}-cdn`,\r\n  WANGSU_CDNPRO: `${ACCESS_PROVIDERS.WANGSU}-cdnpro`,\r\n  WANGSU_CERTIFICATE: `${ACCESS_PROVIDERS.WANGSU}-certificate`,\r\n  WEBHOOK: `${ACCESS_PROVIDERS.WEBHOOK}`,\r\n} as const);\r\n\r\nexport type DeploymentProviderType = (typeof DEPLOYMENT_PROVIDERS)[keyof typeof DEPLOYMENT_PROVIDERS];\r\n\r\nexport const DEPLOYMENT_CATEGORIES = Object.freeze({\r\n  ALL: \"all\",\r\n  CDN: \"cdn\",\r\n  STORAGE: \"storage\",\r\n  LOADBALANCE: \"loadbalance\",\r\n  FIREWALL: \"firewall\",\r\n  AV: \"av\",\r\n  ACCELERATOR: \"accelerator\",\r\n  APIGATEWAY: \"apigw\",\r\n  SERVERLESS: \"serverless\",\r\n  WEBSITE: \"website\",\r\n  SSL: \"ssl\",\r\n  OTHER: \"other\",\r\n} as const);\r\n\r\nexport type DeploymentCategoryType = (typeof DEPLOYMENT_CATEGORIES)[keyof typeof DEPLOYMENT_CATEGORIES];\r\n\r\nexport interface DeploymentProvider extends BaseProviderWithAccess<DeploymentProviderType> {\r\n  category: DeploymentCategoryType;\r\n}\r\n\r\nexport const deploymentProvidersMap: Map<DeploymentProvider[\"type\"] | string, DeploymentProvider> = new Map(\r\n  /*\r\n     注意：此处的顺序决定显示在前端的顺序。\r\n     NOTICE: The following order determines the order displayed at the frontend.\r\n    */\r\n  (\r\n    [\r\n      [DEPLOYMENT_PROVIDERS.LOCAL, \"provider.local\", DEPLOYMENT_CATEGORIES.OTHER, \"builtin\"],\r\n      [DEPLOYMENT_PROVIDERS.SSH, \"provider.ssh\", DEPLOYMENT_CATEGORIES.OTHER],\r\n      [DEPLOYMENT_PROVIDERS.WEBHOOK, \"provider.webhook\", DEPLOYMENT_CATEGORIES.OTHER],\r\n      [DEPLOYMENT_PROVIDERS.KUBERNETES_SECRET, \"provider.kubernetes_secret\", DEPLOYMENT_CATEGORIES.OTHER],\r\n      [DEPLOYMENT_PROVIDERS.S3, \"provider.s3_upload\", DEPLOYMENT_CATEGORIES.STORAGE],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_OSS, \"provider.aliyun_oss\", DEPLOYMENT_CATEGORIES.STORAGE],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_CDN, \"provider.aliyun_cdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_DCDN, \"provider.aliyun_dcdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_ESA, \"provider.aliyun_esa\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_ESA_SAAS, \"provider.aliyun_esa_saas\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_CLB, \"provider.aliyun_clb\", DEPLOYMENT_CATEGORIES.LOADBALANCE],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_ALB, \"provider.aliyun_alb\", DEPLOYMENT_CATEGORIES.LOADBALANCE],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_NLB, \"provider.aliyun_nlb\", DEPLOYMENT_CATEGORIES.LOADBALANCE],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_WAF, \"provider.aliyun_waf\", DEPLOYMENT_CATEGORIES.FIREWALL],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_DDOSPRO, \"provider.aliyun_ddospro\", DEPLOYMENT_CATEGORIES.FIREWALL],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_LIVE, \"provider.aliyun_live\", DEPLOYMENT_CATEGORIES.AV],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_VOD, \"provider.aliyun_vod\", DEPLOYMENT_CATEGORIES.AV],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_FC, \"provider.aliyun_fc\", DEPLOYMENT_CATEGORIES.SERVERLESS],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_APIGW, \"provider.aliyun_apigw\", DEPLOYMENT_CATEGORIES.APIGATEWAY],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_GA, \"provider.aliyun_ga\", DEPLOYMENT_CATEGORIES.ACCELERATOR],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_CAS, \"provider.aliyun_cas_upload\", DEPLOYMENT_CATEGORIES.SSL],\r\n      [DEPLOYMENT_PROVIDERS.ALIYUN_CAS_DEPLOY, \"provider.aliyun_cas_deploy\", DEPLOYMENT_CATEGORIES.SSL],\r\n      [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_COS, \"provider.tencentcloud_cos\", DEPLOYMENT_CATEGORIES.STORAGE],\r\n      [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CDN, \"provider.tencentcloud_cdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_ECDN, \"provider.tencentcloud_ecdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_EO, \"provider.tencentcloud_eo\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CLB, \"provider.tencentcloud_clb\", DEPLOYMENT_CATEGORIES.LOADBALANCE],\r\n      [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_WAF, \"provider.tencentcloud_waf\", DEPLOYMENT_CATEGORIES.FIREWALL],\r\n      [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CSS, \"provider.tencentcloud_css\", DEPLOYMENT_CATEGORIES.AV],\r\n      [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_VOD, \"provider.tencentcloud_vod\", DEPLOYMENT_CATEGORIES.AV],\r\n      [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SCF, \"provider.tencentcloud_scf\", DEPLOYMENT_CATEGORIES.SERVERLESS],\r\n      [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_GAAP, \"provider.tencentcloud_gaap\", DEPLOYMENT_CATEGORIES.ACCELERATOR],\r\n      [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SSL, \"provider.tencentcloud_ssl_upload\", DEPLOYMENT_CATEGORIES.SSL],\r\n      [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SSL_DEPLOY, \"provider.tencentcloud_ssl_deploy\", DEPLOYMENT_CATEGORIES.SSL],\r\n      [DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SSL_UPDATE, \"provider.tencentcloud_ssl_update\", DEPLOYMENT_CATEGORIES.SSL],\r\n      [DEPLOYMENT_PROVIDERS.BAIDUCLOUD_CDN, \"provider.baiducloud_cdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.BAIDUCLOUD_BLB, \"provider.baiducloud_blb\", DEPLOYMENT_CATEGORIES.LOADBALANCE],\r\n      [DEPLOYMENT_PROVIDERS.BAIDUCLOUD_APPBLB, \"provider.baiducloud_appblb\", DEPLOYMENT_CATEGORIES.LOADBALANCE],\r\n      [DEPLOYMENT_PROVIDERS.BAIDUCLOUD_CERT, \"provider.baiducloud_cert_upload\", DEPLOYMENT_CATEGORIES.SSL],\r\n      [DEPLOYMENT_PROVIDERS.HUAWEICLOUD_CDN, \"provider.huaweicloud_cdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.HUAWEICLOUD_OBS, \"provider.huaweicloud_obs\", DEPLOYMENT_CATEGORIES.STORAGE],\r\n      [DEPLOYMENT_PROVIDERS.HUAWEICLOUD_ELB, \"provider.huaweicloud_elb\", DEPLOYMENT_CATEGORIES.LOADBALANCE],\r\n      [DEPLOYMENT_PROVIDERS.HUAWEICLOUD_WAF, \"provider.huaweicloud_waf\", DEPLOYMENT_CATEGORIES.FIREWALL],\r\n      [DEPLOYMENT_PROVIDERS.HUAWEICLOUD_SCM, \"provider.huaweicloud_scm_upload\", DEPLOYMENT_CATEGORIES.SSL],\r\n      [DEPLOYMENT_PROVIDERS.VOLCENGINE_TOS, \"provider.volcengine_tos\", DEPLOYMENT_CATEGORIES.STORAGE],\r\n      [DEPLOYMENT_PROVIDERS.VOLCENGINE_CDN, \"provider.volcengine_cdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.VOLCENGINE_DCDN, \"provider.volcengine_dcdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.VOLCENGINE_CLB, \"provider.volcengine_clb\", DEPLOYMENT_CATEGORIES.LOADBALANCE],\r\n      [DEPLOYMENT_PROVIDERS.VOLCENGINE_ALB, \"provider.volcengine_alb\", DEPLOYMENT_CATEGORIES.LOADBALANCE],\r\n      [DEPLOYMENT_PROVIDERS.VOLCENGINE_WAF, \"provider.volcengine_waf\", DEPLOYMENT_CATEGORIES.FIREWALL],\r\n      [DEPLOYMENT_PROVIDERS.VOLCENGINE_IMAGEX, \"provider.volcengine_imagex\", DEPLOYMENT_CATEGORIES.STORAGE],\r\n      [DEPLOYMENT_PROVIDERS.VOLCENGINE_LIVE, \"provider.volcengine_live\", DEPLOYMENT_CATEGORIES.AV],\r\n      [DEPLOYMENT_PROVIDERS.VOLCENGINE_VOD, \"provider.volcengine_vod\", DEPLOYMENT_CATEGORIES.AV],\r\n      [DEPLOYMENT_PROVIDERS.VOLCENGINE_CERTCENTER, \"provider.volcengine_certcenter_upload\", DEPLOYMENT_CATEGORIES.SSL],\r\n      [DEPLOYMENT_PROVIDERS.JDCLOUD_ALB, \"provider.jdcloud_alb\", DEPLOYMENT_CATEGORIES.LOADBALANCE],\r\n      [DEPLOYMENT_PROVIDERS.JDCLOUD_CDN, \"provider.jdcloud_cdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.JDCLOUD_LIVE, \"provider.jdcloud_live\", DEPLOYMENT_CATEGORIES.AV],\r\n      [DEPLOYMENT_PROVIDERS.JDCLOUD_VOD, \"provider.jdcloud_vod\", DEPLOYMENT_CATEGORIES.AV],\r\n      [DEPLOYMENT_PROVIDERS.QINIU_KODO, \"provider.qiniu_kodo\", DEPLOYMENT_CATEGORIES.STORAGE],\r\n      [DEPLOYMENT_PROVIDERS.QINIU_CDN, \"provider.qiniu_cdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.QINIU_PILI, \"provider.qiniu_pili\", DEPLOYMENT_CATEGORIES.AV],\r\n      [DEPLOYMENT_PROVIDERS.UPYUN_FILE, \"provider.upyun_file\", DEPLOYMENT_CATEGORIES.STORAGE],\r\n      [DEPLOYMENT_PROVIDERS.UPYUN_CDN, \"provider.upyun_cdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.BAISHAN_CDN, \"provider.baishan_cdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.WANGSU_CDN, \"provider.wangsu_cdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.WANGSU_CDNPRO, \"provider.wangsu_cdnpro\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.WANGSU_CERTIFICATE, \"provider.wangsu_certificate_upload\", DEPLOYMENT_CATEGORIES.SSL],\r\n      [DEPLOYMENT_PROVIDERS.DOGECLOUD_CDN, \"provider.dogecloud_cdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.KSYUN_CDN, \"provider.ksyun_cdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.BYTEPLUS_CDN, \"provider.byteplus_cdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.UCLOUD_US3, \"provider.ucloud_us3\", DEPLOYMENT_CATEGORIES.STORAGE],\r\n      [DEPLOYMENT_PROVIDERS.UCLOUD_UCDN, \"provider.ucloud_ucdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.UCLOUD_UCLB, \"provider.ucloud_uclb\", DEPLOYMENT_CATEGORIES.LOADBALANCE],\r\n      [DEPLOYMENT_PROVIDERS.UCLOUD_UALB, \"provider.ucloud_ualb\", DEPLOYMENT_CATEGORIES.LOADBALANCE],\r\n      [DEPLOYMENT_PROVIDERS.UCLOUD_UEWAF, \"provider.ucloud_uewaf\", DEPLOYMENT_CATEGORIES.FIREWALL],\r\n      [DEPLOYMENT_PROVIDERS.UCLOUD_UPATHX, \"provider.ucloud_upathx\", DEPLOYMENT_CATEGORIES.ACCELERATOR],\r\n      [DEPLOYMENT_PROVIDERS.CTCCCLOUD_CDN, \"provider.ctcccloud_cdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.CTCCCLOUD_ICDN, \"provider.ctcccloud_icdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.CTCCCLOUD_AO, \"provider.ctcccloud_ao\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.CTCCCLOUD_ELB, \"provider.ctcccloud_elb\", DEPLOYMENT_CATEGORIES.LOADBALANCE],\r\n      [DEPLOYMENT_PROVIDERS.CTCCCLOUD_LVDN, \"provider.ctcccloud_lvdn\", DEPLOYMENT_CATEGORIES.AV],\r\n      [DEPLOYMENT_PROVIDERS.CTCCCLOUD_FAAS, \"provider.ctcccloud_faas\", DEPLOYMENT_CATEGORIES.SERVERLESS],\r\n      [DEPLOYMENT_PROVIDERS.CTCCCLOUD_CMS, \"provider.ctcccloud_cms_upload\", DEPLOYMENT_CATEGORIES.SSL],\r\n      [DEPLOYMENT_PROVIDERS.RAINYUN_RCDN, \"provider.rainyun_rcdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.RAINYUN_SSLCENTER, \"provider.rainyun_sslcenter_upload\", DEPLOYMENT_CATEGORIES.SSL],\r\n      [DEPLOYMENT_PROVIDERS.UNICLOUD_WEBHOST, \"provider.unicloud_webhost\", DEPLOYMENT_CATEGORIES.WEBSITE],\r\n      [DEPLOYMENT_PROVIDERS.AWS_CLOUDFRONT, \"provider.aws_cloudfront\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.AWS_ACM, \"provider.aws_acm\", DEPLOYMENT_CATEGORIES.SSL],\r\n      [DEPLOYMENT_PROVIDERS.AWS_IAM, \"provider.aws_iam\", DEPLOYMENT_CATEGORIES.SSL],\r\n      [DEPLOYMENT_PROVIDERS.AZURE_KEYVAULT, \"provider.azure_keyvault\", DEPLOYMENT_CATEGORIES.SSL],\r\n      [DEPLOYMENT_PROVIDERS.BUNNY_CDN, \"provider.bunny_cdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.CACHEFLY, \"provider.cachefly\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.GCORE_CDN, \"provider.gcore_cdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.MOHUA_MVH, \"provider.mohua_mvh\", DEPLOYMENT_CATEGORIES.WEBSITE],\r\n      [DEPLOYMENT_PROVIDERS.NETLIFY, \"provider.netlify\", DEPLOYMENT_CATEGORIES.WEBSITE],\r\n      [DEPLOYMENT_PROVIDERS.FLYIO, \"provider.flyio\", DEPLOYMENT_CATEGORIES.WEBSITE],\r\n      [DEPLOYMENT_PROVIDERS.CDNFLY, \"provider.cdnfly\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.FLEXCDN, \"provider.flexcdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.GOEDGE, \"provider.goedge\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS.LECDN, \"provider.lecdn\", DEPLOYMENT_CATEGORIES.CDN],\r\n      [DEPLOYMENT_PROVIDERS[\"1PANEL\"], \"provider.1panel\", DEPLOYMENT_CATEGORIES.WEBSITE],\r\n      [DEPLOYMENT_PROVIDERS[\"1PANEL_CONSOLE\"], \"provider.1panel_console\", DEPLOYMENT_CATEGORIES.OTHER],\r\n      [DEPLOYMENT_PROVIDERS.BAOTAPANEL, \"provider.baotapanel_common\", DEPLOYMENT_CATEGORIES.WEBSITE],\r\n      [DEPLOYMENT_PROVIDERS.BAOTAPANEL_CONSOLE, \"provider.baotapanel_console\", DEPLOYMENT_CATEGORIES.OTHER],\r\n      [DEPLOYMENT_PROVIDERS.BAOTAPANELGO, \"provider.baotapanelgo_common\", DEPLOYMENT_CATEGORIES.WEBSITE],\r\n      [DEPLOYMENT_PROVIDERS.BAOTAPANELGO_CONSOLE, \"provider.baotapanelgo_console\", DEPLOYMENT_CATEGORIES.OTHER],\r\n      [DEPLOYMENT_PROVIDERS.RATPANEL, \"provider.ratpanel_common\", DEPLOYMENT_CATEGORIES.WEBSITE],\r\n      [DEPLOYMENT_PROVIDERS.RATPANEL_CONSOLE, \"provider.ratpanel_console\", DEPLOYMENT_CATEGORIES.OTHER],\r\n      [DEPLOYMENT_PROVIDERS.BAOTAWAF, \"provider.baotawaf_common\", DEPLOYMENT_CATEGORIES.FIREWALL],\r\n      [DEPLOYMENT_PROVIDERS.BAOTAWAF_CONSOLE, \"provider.baotawaf_console\", DEPLOYMENT_CATEGORIES.OTHER],\r\n      [DEPLOYMENT_PROVIDERS.SAFELINE, \"provider.safeline\", DEPLOYMENT_CATEGORIES.FIREWALL],\r\n      [DEPLOYMENT_PROVIDERS.APISIX, \"provider.apisix\", DEPLOYMENT_CATEGORIES.APIGATEWAY],\r\n      [DEPLOYMENT_PROVIDERS.KONG, \"provider.kong\", DEPLOYMENT_CATEGORIES.APIGATEWAY],\r\n      [DEPLOYMENT_PROVIDERS.CPANEL, \"provider.cpanel\", DEPLOYMENT_CATEGORIES.WEBSITE],\r\n      [DEPLOYMENT_PROVIDERS.DOKPLOY, \"provider.dokploy\", DEPLOYMENT_CATEGORIES.WEBSITE],\r\n      [DEPLOYMENT_PROVIDERS.NGINXPROXYMANAGER, \"provider.nginxproxymanager\", DEPLOYMENT_CATEGORIES.WEBSITE],\r\n      [DEPLOYMENT_PROVIDERS.PROXMOXVE, \"provider.proxmoxve\", DEPLOYMENT_CATEGORIES.OTHER],\r\n      [DEPLOYMENT_PROVIDERS.SYNOLOGYDSM, \"provider.synologydsm\", DEPLOYMENT_CATEGORIES.OTHER],\r\n    ] satisfies Array<[DeploymentProviderType, string, DeploymentCategoryType, \"builtin\"] | [DeploymentProviderType, string, DeploymentCategoryType]>\r\n  ).map(([type, name, category, builtin]) => [\r\n    type,\r\n    {\r\n      type: type,\r\n      name: name,\r\n      icon: accessProvidersMap.get(type.split(\"-\")[0])!.icon,\r\n      provider: type.split(\"-\")[0] as AccessProviderType,\r\n      category: category,\r\n      builtin: builtin === \"builtin\",\r\n    },\r\n  ])\r\n);\r\n// #endregion\r\n\r\n// #region NotificationProvider\r\n/*\r\n  注意：如果追加新的常量值，请保持以 ASCII 排序。\r\n  NOTICE: If you add new constant, please keep ASCII order.\r\n */\r\nexport const NOTIFICATION_PROVIDERS = Object.freeze({\r\n  DINGTALKBOT: `${ACCESS_PROVIDERS.DINGTALKBOT}`,\r\n  DISCORDBOT: `${ACCESS_PROVIDERS.DISCORDBOT}`,\r\n  EMAIL: `${ACCESS_PROVIDERS.EMAIL}`,\r\n  LARKBOT: `${ACCESS_PROVIDERS.LARKBOT}`,\r\n  MATTERMOST: `${ACCESS_PROVIDERS.MATTERMOST}`,\r\n  SLACKBOT: `${ACCESS_PROVIDERS.SLACKBOT}`,\r\n  TELEGRAMBOT: `${ACCESS_PROVIDERS.TELEGRAMBOT}`,\r\n  WEBHOOK: `${ACCESS_PROVIDERS.WEBHOOK}`,\r\n  WECOMBOT: `${ACCESS_PROVIDERS.WECOMBOT}`,\r\n} as const);\r\n\r\nexport type NotificationProviderType = (typeof NOTIFICATION_PROVIDERS)[keyof typeof NOTIFICATION_PROVIDERS];\r\n\r\nexport interface NotificationProvider extends BaseProviderWithAccess<NotificationProviderType> {}\r\n\r\nexport const notificationProvidersMap: Map<NotificationProvider[\"type\"] | string, NotificationProvider> = new Map(\r\n  /*\r\n    注意：此处的顺序决定显示在前端的顺序。\r\n    NOTICE: The following order determines the order displayed at the frontend.\r\n   */\r\n  (\r\n    [\r\n      [NOTIFICATION_PROVIDERS.EMAIL],\r\n      [NOTIFICATION_PROVIDERS.WEBHOOK],\r\n      [NOTIFICATION_PROVIDERS.DINGTALKBOT],\r\n      [NOTIFICATION_PROVIDERS.LARKBOT],\r\n      [NOTIFICATION_PROVIDERS.WECOMBOT],\r\n      [NOTIFICATION_PROVIDERS.DISCORDBOT],\r\n      [NOTIFICATION_PROVIDERS.SLACKBOT],\r\n      [NOTIFICATION_PROVIDERS.TELEGRAMBOT],\r\n      [NOTIFICATION_PROVIDERS.MATTERMOST],\r\n    ] satisfies Array<[NotificationProviderType]>\r\n  ).map(([type]) => [\r\n    type,\r\n    {\r\n      type: type,\r\n      name: accessProvidersMap.get(type.split(\"-\")[0])!.name,\r\n      icon: accessProvidersMap.get(type.split(\"-\")[0])!.icon,\r\n      provider: type.split(\"-\")[0] as AccessProviderType,\r\n      builtin: false,\r\n    },\r\n  ])\r\n);\r\n// #endregion\r\n"
  },
  {
    "path": "ui/src/domain/settings.ts",
    "content": "import { type CAProviderType } from \"./provider\";\n\nexport const SETTINGS_NAMES = Object.freeze({\n  EMAILS: \"emails\",\n  NOTIFY_TEMPLATE: \"notifyTemplate\",\n  SCRIPT_TEMPLATE: \"scriptTemplate\",\n  SSL_PROVIDER: \"sslProvider\",\n  PERSISTENCE: \"persistence\",\n} as const);\n\nexport type SettingsNames = (typeof SETTINGS_NAMES)[keyof typeof SETTINGS_NAMES];\n\nexport interface SettingsModel<T extends NonNullable<unknown> = any> extends BaseModel {\n  name: string;\n  content: T;\n}\n\n// #region Settings: Emails\nexport type EmailsSettingsContent = {\n  emails: string[];\n};\n// #endregion\n\n// #region Settings: NotifyTemplate\nexport type NotifyTemplateContent = {\n  templates: Array<{\n    name: string;\n    subject: string;\n    message: string;\n  }>;\n};\n// #endregion\n\n// #region Settings: ScriptTemplate\nexport type ScriptTemplateContent = {\n  templates: Array<{\n    name: string;\n    command: string;\n  }>;\n};\n// #endregion\n\n// #region Settings: SSLProvider\nexport type SSLProviderSettingsContent = {\n  provider: CAProviderType;\n  configs: {\n    [key: string]: Record<string, unknown> | undefined;\n  };\n  timeout?: number;\n};\n// #endregion\n\n// #region Settings: Persistence\nexport type PersistenceSettingsContent = {\n  certificatesWarningDaysBeforeExpire?: number;\n  certificatesRetentionMaxDays?: number;\n  workflowRunsRetentionMaxDays?: number;\n};\n// #endregion\n"
  },
  {
    "path": "ui/src/domain/statistics.ts",
    "content": "export type Statistics = {\n  certificateTotal: number;\n  certificateExpired: number;\n  certificateExpiringSoon: number;\n  workflowTotal: number;\n  workflowEnabled: number;\n  workflowDisabled: number;\n};\n"
  },
  {
    "path": "ui/src/domain/workflow.ts",
    "content": "import { getI18n } from \"react-i18next\";\nimport { Immer } from \"immer\";\nimport { nanoid } from \"nanoid\";\n\nimport { type WorkflowRunModel } from \"./workflowRun\";\n\nexport interface WorkflowModel extends BaseModel {\n  name: string;\n  description?: string;\n  trigger: string;\n  triggerCron?: string;\n  enabled?: boolean;\n  graphDraft?: WorkflowGraph;\n  graphContent?: WorkflowGraph;\n  hasDraft?: boolean;\n  hasContent?: boolean;\n  lastRunRef?: string;\n  lastRunStatus?: string;\n  lastRunTime?: string;\n  expand?: {\n    lastRunRef?: Pick<WorkflowRunModel, \"id\" | \"status\" | \"trigger\" | \"startedAt\" | \"endedAt\" | \"error\">;\n  };\n}\n\nexport interface WorkflowGraph {\n  nodes: WorkflowNode[];\n}\n\nexport const WORKFLOW_TRIGGERS = Object.freeze({\n  SCHEDULED: \"scheduled\",\n  MANUAL: \"manual\",\n} as const);\n\nexport type WorkflowTriggerType = (typeof WORKFLOW_TRIGGERS)[keyof typeof WORKFLOW_TRIGGERS];\n\n// #region Node\nexport const WORKFLOW_NODE_TYPES = Object.freeze({\n  START: \"start\",\n  END: \"end\",\n  DELAY: \"delay\",\n  CONDITION: \"condition\",\n  BRANCHBLOCK: \"branchBlock\",\n  TRYCATCH: \"tryCatch\",\n  TRYBLOCK: \"tryBlock\",\n  CATCHBLOCK: \"catchBlock\",\n  BIZ_APPLY: \"bizApply\",\n  BIZ_UPLOAD: \"bizUpload\",\n  BIZ_MONITOR: \"bizMonitor\",\n  BIZ_DEPLOY: \"bizDeploy\",\n  BIZ_NOTIFY: \"bizNotify\",\n} as const);\n\nexport type WorkflowNodeType = (typeof WORKFLOW_NODE_TYPES)[keyof typeof WORKFLOW_NODE_TYPES];\n\nexport type WorkflowNode = {\n  id: string;\n  type: WorkflowNodeType;\n  data: {\n    name?: string;\n    disabled?: boolean;\n    config?: Record<string, unknown>;\n    [key: string]: unknown;\n  };\n  blocks?: WorkflowNode[];\n};\n\nexport type WorkflowNodeConfigForStart = {\n  trigger: string;\n  triggerCron?: string;\n};\n\nexport const defaultNodeConfigForStart = (): Partial<WorkflowNodeConfigForStart> => {\n  return {\n    trigger: WORKFLOW_TRIGGERS.MANUAL,\n  };\n};\n\nexport type WorkflowNodeConfigForDelay = {\n  wait?: number;\n};\n\nexport const defaultNodeConfigForDelay = (): Partial<WorkflowNodeConfigForDelay> => {\n  return {};\n};\n\nexport type WorkflowNodeConfigForBranchBlock = {\n  expression?: Expr;\n};\n\nexport const defaultNodeConfigForBranchBlock = (): Partial<WorkflowNodeConfigForBranchBlock> => {\n  return {};\n};\n\nexport type WorkflowNodeConfigForBizApply = {\n  identifier: \"domain\" | \"ip\";\n  domains: string;\n  ipaddrs: string;\n  contactEmail: string;\n  challengeType: string;\n  provider: string;\n  providerAccessId: string;\n  providerConfig?: Record<string, unknown>;\n  caProvider?: string;\n  caProviderAccessId?: string;\n  caProviderConfig?: Record<string, unknown>;\n  keySource: string;\n  keyAlgorithm: string;\n  keyContent?: string;\n  validityLifetime?: string;\n  acmeProfile?: string;\n  nameservers?: string;\n  dnsPropagationWait?: number;\n  dnsPropagationTimeout?: number;\n  dnsTTL?: number;\n  httpDelayWait?: number;\n  disableFollowCNAME?: boolean;\n  disableARI?: boolean;\n  skipBeforeExpiryDays: number;\n};\n\nexport const defaultNodeConfigForBizApply = (): Partial<WorkflowNodeConfigForBizApply> => {\n  return {\n    challengeType: \"dns-01\" as const,\n    keySource: \"auto\" as const,\n    keyAlgorithm: \"RSA2048\" as const,\n    skipBeforeExpiryDays: 30,\n  };\n};\n\nexport type WorkflowNodeConfigForBizUpload = {\n  source: string;\n  certificate: string;\n  privateKey: string;\n};\n\nexport const defaultNodeConfigForBizUpload = (): Partial<WorkflowNodeConfigForBizUpload> => {\n  return {\n    source: \"form\" as const,\n  };\n};\n\nexport type WorkflowNodeConfigForBizMonitor = {\n  host: string;\n  port: number;\n  domain?: string;\n  requestPath?: string;\n};\n\nexport const defaultNodeConfigForBizMonitor = (): Partial<WorkflowNodeConfigForBizMonitor> => {\n  return {\n    host: \"\",\n    port: 443,\n    requestPath: \"/\",\n  };\n};\n\nexport type WorkflowNodeConfigForBizDeploy = {\n  certificateOutputNodeId: string;\n  provider: string;\n  providerAccessId?: string;\n  providerConfig?: Record<string, unknown>;\n  skipOnLastSucceeded: boolean;\n};\n\nexport const defaultNodeConfigForBizDeploy = (): Partial<WorkflowNodeConfigForBizDeploy> => {\n  return {\n    skipOnLastSucceeded: true,\n  };\n};\n\nexport type WorkflowNodeConfigForBizNotify = {\n  subject: string;\n  message: string;\n  provider: string;\n  providerAccessId: string;\n  providerConfig?: Record<string, unknown>;\n  skipOnAllPrevSkipped?: boolean;\n};\n\nexport const defaultNodeConfigForBizNotify = (): Partial<WorkflowNodeConfigForBizNotify> => {\n  return {};\n};\n\nexport const newNodeId = (): string => {\n  return nanoid()\n    .replace(/^[_-]+/g, \"\")\n    .replace(/[_-]+$/g, \"\");\n};\n\nexport const newNode = (type: WorkflowNodeType, { i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }): WorkflowNode => {\n  const { t } = i18n;\n\n  switch (type) {\n    case WORKFLOW_NODE_TYPES.START:\n      return {\n        id: newNodeId(),\n        type: type,\n        data: {\n          name: t(\"workflow_node.start.default_name\"),\n          config: defaultNodeConfigForStart(),\n        },\n      };\n\n    case WORKFLOW_NODE_TYPES.END:\n      return {\n        id: newNodeId(),\n        type: type,\n        data: {\n          name: t(\"workflow_node.end.default_name\"),\n        },\n      };\n\n    case WORKFLOW_NODE_TYPES.DELAY:\n      return {\n        id: newNodeId(),\n        type: type,\n        data: {\n          name: t(\"workflow_node.delay.default_name\"),\n          config: defaultNodeConfigForDelay(),\n        },\n      };\n\n    case WORKFLOW_NODE_TYPES.CONDITION: {\n      const branch1 = newNode(WORKFLOW_NODE_TYPES.BRANCHBLOCK, { i18n });\n      branch1.data.name = `${branch1.data.name} 1`;\n      const branch2 = newNode(WORKFLOW_NODE_TYPES.BRANCHBLOCK, { i18n });\n      branch2.data.name = `${branch2.data.name} 2`;\n\n      return {\n        id: newNodeId(),\n        type: type,\n        data: {\n          name: t(\"workflow_node.condition.default_name\"),\n          config: defaultNodeConfigForBranchBlock(),\n        },\n        blocks: [branch1, branch2],\n      };\n    }\n\n    case WORKFLOW_NODE_TYPES.BRANCHBLOCK:\n      return {\n        id: newNodeId(),\n        type: type,\n        blocks: [],\n        data: {\n          name: t(\"workflow_node.branch_block.default_name\"),\n          config: defaultNodeConfigForBranchBlock(),\n        },\n      };\n\n    case WORKFLOW_NODE_TYPES.TRYCATCH:\n      return {\n        id: newNodeId(),\n        type: type,\n        data: {\n          name: t(\"workflow_node.try_catch.default_name\"),\n        },\n        blocks: [newNode(WORKFLOW_NODE_TYPES.TRYBLOCK, { i18n }), newNode(WORKFLOW_NODE_TYPES.CATCHBLOCK, { i18n })],\n      };\n\n    case WORKFLOW_NODE_TYPES.TRYBLOCK:\n      return {\n        id: newNodeId(),\n        type: type,\n        data: {\n          name: \"\",\n        },\n        blocks: [],\n      };\n\n    case WORKFLOW_NODE_TYPES.CATCHBLOCK:\n      return {\n        id: newNodeId(),\n        type: type,\n        blocks: [newNode(WORKFLOW_NODE_TYPES.END, { i18n })],\n        data: {\n          name: t(\"workflow_node.catch_block.default_name\"),\n        },\n      };\n\n    case WORKFLOW_NODE_TYPES.BIZ_APPLY:\n      return {\n        id: newNodeId(),\n        type: type,\n        data: {\n          name: t(\"workflow_node.apply.default_name\"),\n          config: defaultNodeConfigForBizApply(),\n        },\n      };\n\n    case WORKFLOW_NODE_TYPES.BIZ_UPLOAD:\n      return {\n        id: newNodeId(),\n        type: type,\n        data: {\n          name: t(\"workflow_node.upload.default_name\"),\n          config: defaultNodeConfigForBizUpload(),\n        },\n      };\n\n    case WORKFLOW_NODE_TYPES.BIZ_MONITOR:\n      return {\n        id: newNodeId(),\n        type: type,\n        data: {\n          name: t(\"workflow_node.monitor.default_name\"),\n          config: defaultNodeConfigForBizMonitor(),\n        },\n      };\n\n    case WORKFLOW_NODE_TYPES.BIZ_DEPLOY:\n      return {\n        id: newNodeId(),\n        type: type,\n        data: {\n          name: t(\"workflow_node.deploy.default_name\"),\n          config: defaultNodeConfigForBizDeploy(),\n        },\n      };\n\n    case WORKFLOW_NODE_TYPES.BIZ_NOTIFY:\n      return {\n        id: newNodeId(),\n        type: type,\n        data: {\n          name: t(\"workflow_node.notify.default_name\"),\n          config: defaultNodeConfigForBizNotify(),\n        },\n      };\n\n    default:\n      throw new Error(\"Invalid value of `nodeType`\");\n  }\n};\n\nexport const duplicateNode = (node: WorkflowNode, options?: { withCopySuffix?: boolean }) => {\n  return duplicateNodes([node], options)[0];\n};\n\nexport const duplicateNodes = (nodes: WorkflowNode[], options?: { withCopySuffix?: boolean }) => {\n  function duplicate(node: WorkflowNode, { withCopySuffix, nodeIdMap }: { withCopySuffix: boolean; nodeIdMap: Map<string, string> }) {\n    const { produce } = new Immer({ autoFreeze: false });\n    return produce(node, (draft) => {\n      draft.data ??= {};\n      draft.id = newNodeId();\n      draft.data.name = withCopySuffix ? `${draft.data?.name || \"\"}-copy` : `${draft.data?.name || \"\"}`;\n\n      nodeIdMap.set(node.id, draft.id); // 原节点 ID 映射到新节点 ID\n\n      if (draft.blocks) {\n        draft.blocks = draft.blocks.map((block) => duplicate(block, { withCopySuffix: false, nodeIdMap }));\n      }\n\n      if (draft.data?.config) {\n        switch (draft.type) {\n          case WORKFLOW_NODE_TYPES.BIZ_DEPLOY:\n            {\n              const prevNodeId = draft.data.config.certificateOutputNodeId as string;\n              if (nodeIdMap.has(prevNodeId)) {\n                draft.data.config = {\n                  ...draft.data.config,\n                  certificateOutputNodeId: nodeIdMap.get(prevNodeId),\n                };\n              }\n            }\n            break;\n\n          case WORKFLOW_NODE_TYPES.BRANCHBLOCK:\n            {\n              const stack = [] as Expr[];\n              const expr = draft.data.config.expression as Expr;\n              if (expr) {\n                stack.push(expr);\n                while (stack.length > 0) {\n                  const n = stack.pop()!;\n                  if (\"left\" in n) {\n                    stack.push(n.left);\n                    if (\"selector\" in n.left) {\n                      const prevNodeId = n.left.selector.id;\n                      if (nodeIdMap.has(prevNodeId)) {\n                        n.left.selector.id = nodeIdMap.get(prevNodeId)!;\n                      }\n                    }\n                  }\n                  if (\"right\" in n) {\n                    stack.push(n.right);\n                  }\n                }\n\n                draft.data.config = {\n                  ...draft.data.config,\n                  expression: expr,\n                };\n              }\n            }\n            break;\n        }\n      }\n\n      return draft;\n    });\n  }\n\n  const map = new Map<string, string>();\n  return nodes.map((node) => duplicate(node, { withCopySuffix: options?.withCopySuffix ?? true, nodeIdMap: map }));\n};\n// #endregion\n\n// #region Expression\nexport enum ExprType {\n  Constant = \"const\",\n  Variant = \"var\",\n  Comparison = \"comparison\",\n  Logical = \"logical\",\n  Not = \"not\",\n}\n\nexport type ExprValue = string | number | boolean;\nexport type ExprValueType = \"string\" | \"number\" | \"boolean\";\nexport type ExprValueSelector = {\n  id: string;\n  name: string;\n  type: ExprValueType;\n};\n\nexport type ExprComparisonOperator = \"gt\" | \"gte\" | \"lt\" | \"lte\" | \"eq\" | \"neq\";\nexport type ExprLogicalOperator = \"and\" | \"or\" | \"not\";\n\nexport type ConstantExpr = { type: ExprType.Constant; value: string; valueType: ExprValueType };\nexport type VariantExpr = { type: ExprType.Variant; selector: ExprValueSelector };\nexport type ComparisonExpr = { type: ExprType.Comparison; operator: ExprComparisonOperator; left: Expr; right: Expr };\nexport type LogicalExpr = { type: ExprType.Logical; operator: ExprLogicalOperator; left: Expr; right: Expr };\nexport type NotExpr = { type: ExprType.Not; expr: Expr };\nexport type Expr = ConstantExpr | VariantExpr | ComparisonExpr | LogicalExpr | NotExpr;\n// #endregion\n"
  },
  {
    "path": "ui/src/domain/workflowLog.ts",
    "content": "export interface WorkflowLogModel extends Omit<BaseModel, \"updated\"> {\n  nodeId: string;\n  nodeName: string;\n  timestamp: ReturnType<typeof Date.prototype.getTime>;\n  level: number;\n  message: string;\n  data: Record<string, any>;\n}\n\nexport enum WorkflowLogLevel {\n  Debug = -4,\n  Info = 0,\n  Warn = 4,\n  Error = 8,\n}\n"
  },
  {
    "path": "ui/src/domain/workflowRun.ts",
    "content": "import { type WorkflowGraph, type WorkflowModel } from \"./workflow\";\n\nexport interface WorkflowRunModel extends BaseModel {\n  workflowRef: string;\n  status: string;\n  trigger: string;\n  startedAt: ISO8601String;\n  endedAt: ISO8601String;\n  graph?: WorkflowGraph;\n  error?: string;\n  outputs?: Array<{\n    type: string;\n    name: string;\n    value: string;\n    valueType: string;\n  }>;\n  expand?: {\n    workflowRef?: Pick<WorkflowModel, \"id\" | \"name\" | \"description\">;\n  };\n}\n\nexport const WORKFLOW_RUN_STATUSES = Object.freeze({\n  PENDING: \"pending\",\n  PROCESSING: \"processing\",\n  SUCCEEDED: \"succeeded\",\n  FAILED: \"failed\",\n  CANCELED: \"canceled\",\n} as const);\n\nexport type WorkflorRunStatusType = (typeof WORKFLOW_RUN_STATUSES)[keyof typeof WORKFLOW_RUN_STATUSES];\n"
  },
  {
    "path": "ui/src/global.css",
    "content": "/* NOTICE: There are layer conflicts between tailwindcss v4 and antd, hack for this. */\n/* @layer theme, base, antd, components, utilities; */\n/* @import \"tailwindcss\"; */\n\n@import \"tailwindcss/theme.css\";\n@import \"tailwindcss/utilities.css\";\n@import \"antd/dist/reset.css\";\n\n@custom-variant dark (&:where(.dark, .dark *));\n@source inline(\"gap-{0,{1..96}}\");\n@source inline(\"grid-cols-{0,{1..12}}\");\n\n@theme {\n  --color-background: var(---twColorBackground);\n  --color-foreground: var(---twColorForeground);\n  --color-primary: var(---twColorPrimary);\n  --color-info: var(---twColorInfo);\n  --color-success: var(---twColorSuccess);\n  --color-warning: var(---twColorWarning);\n  --color-error: var(---twColorError);\n}\n\n@layer base {\n  :root {\n    ---twColorBackground: #ffffff;\n    ---twColorForeground: #141414;\n    ---twColorPrimary: #f97316;\n    ---twColorInfo: #478ce6;\n    ---twColorSuccess: #59ab5c;\n    ---twColorWarning: #daa93e;\n    ---twColorError: #e5534b;\n  }\n\n  .dark {\n    ---twColorBackground: #17191c;\n    ---twColorForeground: #fafaf9;\n    ---twColorPrimary: #ea580c;\n    ---twColorInfo: #0969da;\n    ---twColorSuccess: #1a7f37;\n    ---twColorWarning: #eac54f;\n    ---twColorError: #d1242f;\n  }\n}\n\n@layer base {\n  /* Reset ol, ul, dl */\n  ol,\n  ul,\n  dl {\n    margin-left: 0;\n    margin-right: 0;\n    padding-left: 1.25em;\n  }\n\n  /* Fix non-antd icon's position not correct */\n  .ant-btn > .ant-btn-icon,\n  svg.tabler-icon,\n  svg.icon {\n    line-height: 1;\n  }\n\n  /* Fix antd drawer title overflow */\n  .ant-drawer .ant-drawer-title {\n    overflow: hidden;\n  }\n}\n\n@layer components {\n  .container {\n    @apply mx-auto max-w-320;\n  }\n}\n"
  },
  {
    "path": "ui/src/hooks/index.ts",
    "content": "﻿import useAntdForm from \"./useAntdForm\";\nimport useAntdFormName from \"./useAntdFormName\";\nimport useAppSettings from \"./useAppSettings\";\nimport useBrowserTheme from \"./useBrowserTheme\";\nimport useTriggerElement from \"./useTriggerElement\";\nimport useVersionChecker from \"./useVersionChecker\";\nimport useZustandShallowSelector from \"./useZustandShallowSelector\";\n\nexport { useAntdForm, useAntdFormName, useAppSettings, useBrowserTheme, useTriggerElement, useVersionChecker, useZustandShallowSelector };\n"
  },
  {
    "path": "ui/src/hooks/useAntdForm.ts",
    "content": "﻿import { useState } from \"react\";\nimport { useDeepCompareEffect } from \"ahooks\";\nimport { Form, type FormInstance, type FormProps } from \"antd\";\n\nimport useAntdFormName from \"./useAntdFormName\";\n\nexport interface UseAntdFormOptions<T extends NonNullable<unknown> = any> {\n  form?: FormInstance<T>;\n  initialValues?: Partial<T | any> | (() => Partial<T | any> | Promise<Partial<T | any>>);\n  name?: string;\n  onSubmit?: (values: T) => unknown | Promise<unknown>;\n}\n\nexport interface UseAntdFormReturns<T extends NonNullable<unknown> = any> {\n  form: FormInstance<T>;\n  formProps: Omit<FormProps<T>, \"children\">;\n  formPending: boolean;\n  submit: (values?: T) => Promise<unknown>;\n}\n\n/**\n * 生成并获取一个 antd 表单的实例、属性等。\n * 通常为配合 Form 组件使用，以减少样板代码。\n * @param {UseAntdFormOptions} options\n * @returns {UseAntdFormReturns}\n */\nconst useAntdForm = <T extends NonNullable<unknown> = any>({ form, initialValues, onSubmit, ...options }: UseAntdFormOptions<T>): UseAntdFormReturns<T> => {\n  const formInst = form ?? Form[\"useForm\"]()[0];\n  const formName = useAntdFormName({ form: formInst, name: options.name });\n  const [formInitialValues, setFormInitialValues] = useState<Partial<T>>();\n  const [formPending, setFormPending] = useState(false);\n\n  useDeepCompareEffect(() => {\n    let unmounted = false;\n\n    if (!initialValues) {\n      return;\n    }\n\n    let p: Promise<Partial<T>>;\n    if (typeof initialValues === \"function\") {\n      p = Promise.resolve(initialValues());\n    } else {\n      p = Promise.resolve(initialValues);\n    }\n\n    p.then((res) => {\n      if (!unmounted) {\n        type FieldName = Parameters<FormInstance<T>[\"getFieldValue\"]>[0];\n        type FieldsValue = Parameters<FormInstance<T>[\"setFieldsValue\"]>[0];\n\n        const obj = { ...res };\n        Object.keys(res).forEach((key) => {\n          obj[key as keyof T] = formInst!.isFieldTouched(key as FieldName) ? formInst!.getFieldValue(key as FieldName) : res[key as keyof T];\n        });\n\n        setFormInitialValues(res);\n        formInst!.setFieldsValue(obj as FieldsValue);\n      }\n    });\n\n    return () => {\n      unmounted = true;\n    };\n  }, [formInst, initialValues]);\n\n  const onFinish = (values: T) => {\n    if (formPending) return Promise.reject(new Error(\"Form is pending\"));\n\n    setFormPending(true);\n\n    return new Promise((resolve, reject) => {\n      formInst\n        .validateFields()\n        .then(() => {\n          resolve(\n            Promise.resolve(onSubmit?.(values))\n              .then((ret) => {\n                setFormPending(false);\n                return ret;\n              })\n              .catch((err) => {\n                setFormPending(false);\n                throw err;\n              })\n          );\n        })\n        .catch((err) => {\n          setFormPending(false);\n          reject(err);\n        });\n    });\n  };\n\n  const formProps: FormProps = {\n    form: formInst,\n    initialValues: formInitialValues,\n    name: formName,\n    onFinish,\n  };\n\n  return {\n    form: formInst,\n    formProps: formProps,\n    formPending: formPending,\n    submit: () => {\n      return onFinish(formInst.getFieldsValue(true));\n    },\n  };\n};\n\nexport default useAntdForm;\n"
  },
  {
    "path": "ui/src/hooks/useAntdFormName.ts",
    "content": "﻿import { useCreation } from \"ahooks\";\nimport { type FormInstance } from \"antd\";\nimport { nanoid } from \"nanoid/non-secure\";\n\nexport interface UseAntdFormNameOptions<T extends NonNullable<unknown> = any> {\n  form: FormInstance<T>;\n  name?: string;\n}\n\n/**\n * 生成并获取一个 antd 表单的唯一名称。\n * 通常为配合 Form 组件使用，避免页面上同时存在多个表单时若有同名的 FormItem 会产生冲突。\n * @param {UseAntdFormNameOptions} options\n * @returns {string}\n */\nconst useAntdFormName = <T extends NonNullable<unknown> = any>(options: UseAntdFormNameOptions<T>) => {\n  const formName = useCreation(() => `${options.name}_${nanoid()}`, [options.name, options.form]);\n  return formName;\n};\n\nexport default useAntdFormName;\n"
  },
  {
    "path": "ui/src/hooks/useAppSettings.ts",
    "content": "﻿import { useCallback } from \"react\";\nimport { useLocalStorageState } from \"ahooks\";\n\nconst LOCAL_STORAGE_KEY = \"certimate-ui-appsettings\";\nif (!localStorage.getItem(LOCAL_STORAGE_KEY)) {\n  localStorage.setItem(LOCAL_STORAGE_KEY, \"{}\");\n}\n\ntype AppSettings = {\n  /**\n   * 每页显示的默认条目数。\n   */\n  defaultPerPage?: number;\n  /**\n   * 工作流默认布局。\n   */\n  defaultWorkflowLayout?: \"horizontal\" | \"vertical\";\n};\n\nexport type UseAppSettingsReturns = {\n  appSettings: AppSettings;\n  setAppSettings: (value: AppSettings) => void;\n  resetAppSettings: () => void;\n};\n\n/**\n * 获取并设置当前应用全局配置。\n * @returns {UseAppSettingsReturns}\n */\nconst useAppSettings = (): UseAppSettingsReturns => {\n  const [state, setState] = useLocalStorageState<AppSettings>(LOCAL_STORAGE_KEY, {\n    defaultValue: {},\n  });\n  state.defaultPerPage ??= 15;\n  state.defaultWorkflowLayout ??= \"vertical\";\n\n  const resetState = useCallback(() => {\n    localStorage.removeItem(LOCAL_STORAGE_KEY);\n    setState({});\n  }, [setState]);\n\n  return {\n    appSettings: state,\n    setAppSettings: setState,\n    resetAppSettings: resetState,\n  };\n};\n\nexport default useAppSettings;\n"
  },
  {
    "path": "ui/src/hooks/useBrowserTheme.ts",
    "content": "﻿import { useTheme } from \"ahooks\";\n\nconst LOCAL_STORAGE_KEY = \"certimate-ui-theme\";\nif (!localStorage.getItem(LOCAL_STORAGE_KEY)) {\n  localStorage.setItem(LOCAL_STORAGE_KEY, \"dark\");\n}\n\nexport type UseBrowserThemeReturns = ReturnType<typeof useTheme>;\n\n/**\n * 获取并设置当前浏览器系统主题。\n * @returns {UseBrowserThemeReturns}\n */\nconst useBrowserTheme = (): UseBrowserThemeReturns => {\n  return useTheme({ localStorageKey: LOCAL_STORAGE_KEY });\n};\n\nexport default useBrowserTheme;\n"
  },
  {
    "path": "ui/src/hooks/useTriggerElement.ts",
    "content": "﻿import { Fragment, cloneElement, createElement, isValidElement, useMemo } from \"react\";\n\nexport type UseTriggerElementOptions = {\n  onClick?: (e: React.MouseEvent) => void;\n};\n\n/**\n * 获取一个触发器元素。\n * 通常为配合 Drawer、Modal 等组件使用。\n * @param {React.ReactNode} trigger\n * @param {UseTriggerElementOptions} [options]\n * @returns {React.ReactElement}\n */\nconst useTriggerElement = (trigger: React.ReactNode, options?: UseTriggerElementOptions) => {\n  const onClick = options?.onClick;\n  const triggerDom = useMemo(() => {\n    if (!trigger) {\n      return null;\n    }\n\n    const el = isValidElement(trigger) ? trigger : createElement(Fragment, null, trigger);\n    return cloneElement(el, {\n      ...el.props,\n      onClick: (e: React.MouseEvent) => {\n        onClick?.(e);\n        el.props?.onClick?.(e);\n      },\n    });\n  }, [trigger, onClick]);\n  return triggerDom;\n};\n\nexport default useTriggerElement;\n"
  },
  {
    "path": "ui/src/hooks/useVersionChecker.ts",
    "content": "﻿import { useEffect, useState } from \"react\";\nimport { useRequest } from \"ahooks\";\n\nimport { APP_VERSION } from \"@/domain/app\";\n\nexport type UseVersionCheckerReturns = {\n  hasUpdate: boolean;\n  checkUpdate: () => Promise<boolean>;\n};\n\nconst extractSemver = (vers: string) => {\n  let semver = String(vers ?? \"\");\n  semver = semver.replace(/^v/i, \"\");\n  semver = semver.split(\"-\")[0];\n  return semver;\n};\n\nconst compareVersions = (a: string, b: string) => {\n  const aSemver = extractSemver(a);\n  const bSemver = extractSemver(b);\n  const aSemverParts = aSemver.split(\".\");\n  const bSemverParts = bSemver.split(\".\");\n\n  const len = Math.max(aSemverParts.length, bSemverParts.length);\n  for (let i = 0; i < len; i++) {\n    const aPart = parseInt(aSemverParts[i] ?? \"0\");\n    const bPart = parseInt(bSemverParts[i] ?? \"0\");\n    if (aPart > bPart) return 1;\n    if (bPart > aPart) return -1;\n  }\n\n  return 0;\n};\n\nconst LOCAL_STORAGE_KEY = \"certimate-ui-newver\";\n\n/**\n * 获取版本检查器。\n * @returns {UseVersionCheckerReturns}\n */\nconst useVersionChecker = () => {\n  const [hasUpdate, setHasUpdate] = useState(() => {\n    const newver = localStorage.getItem(LOCAL_STORAGE_KEY)!;\n    if (newver) {\n      return compareVersions(newver, APP_VERSION) === 1;\n    }\n\n    return false;\n  });\n\n  const { refresh, cancel } = useRequest(\n    async () => {\n      type ReleaseInfo = {\n        id: number;\n        name: string;\n        body: string;\n        prerelease: boolean;\n      };\n\n      let releases: ReleaseInfo[] = [];\n      try {\n        // try to fetch from GitHub\n        releases = await fetch(\"https://api.github.com/repos/certimate-go/certimate/releases\").then((res) => {\n          if (res.ok) {\n            return res.json().then((res) => Array.from(res) as ReleaseInfo[]);\n          } else {\n            throw new Error(\"Failed to check update from GitHub\");\n          }\n        });\n      } catch {\n        // fallback to fetch from Gitee\n        releases = await fetch(\"https://gitee.com/api/v5/repos/certimate-go/certimate/releases\").then((res) => {\n          if (res.ok) {\n            return res.json().then((res) => Array.from(res) as ReleaseInfo[]);\n          } else {\n            throw new Error(\"Failed to check update from GitHub\");\n          }\n        });\n      }\n\n      const cIdx = releases.findIndex((e) => e.name === APP_VERSION);\n      if (cIdx === 0) {\n        return false;\n      }\n\n      const nIdx = releases.findIndex((e) => compareVersions(e.name, APP_VERSION) !== -1);\n      if (cIdx !== -1 && cIdx <= nIdx) {\n        return false;\n      }\n\n      if (releases[nIdx]) {\n        localStorage.setItem(LOCAL_STORAGE_KEY, releases[nIdx].name);\n      } else {\n        localStorage.removeItem(LOCAL_STORAGE_KEY);\n      }\n\n      return !!releases[nIdx];\n    },\n    {\n      manual: true,\n      focusTimespan: 15 * 60 * 1000,\n      pollingInterval: 6 * 60 * 60 * 1000,\n      throttleWait: 60 * 1000,\n      onSuccess: (res) => {\n        setHasUpdate(res);\n      },\n    }\n  );\n\n  useEffect(() => {\n    refresh();\n\n    return () => cancel();\n  }, []);\n\n  return {\n    hasUpdate: hasUpdate,\n    checkUpdate: refresh,\n  };\n};\n\nexport default useVersionChecker;\n"
  },
  {
    "path": "ui/src/hooks/useZustandShallowSelector.ts",
    "content": "﻿import { useRef } from \"react\";\nimport { isArray, pick } from \"radash\";\nimport { shallow } from \"zustand/shallow\";\n\ntype MaybeMany<T> = T | readonly T[];\n\nexport type UseZustandShallowSelectorReturns<T extends object, TKeys extends keyof T> = (state: T) => Pick<T, TKeys>;\n\n/**\n * 选择并获取指定的状态。\n * 基于 `zustand.useShallow` 二次封装，以减少样板代码。\n * @param {Array} paths 要选择的状态键名。\n * @returns {UseZustandShallowSelectorReturns}\n *\n * @example\n * ```js\n * // 使用示例：\n * const { foo, bar, baz } = useStore(useZustandShallowSelector([\"foo\", \"bar\", \"baz\"]));\n *\n * // 以上代码等效于：\n * const { foo, bar, baz } = useStore(useShallow((state) => ({\n *  foo: state.foo,\n *  bar: state.bar,\n *  baz: state.baz,\n * })));\n * ```\n */\nconst useZustandShallowSelector = <T extends object, TKeys extends keyof T>(paths: MaybeMany<TKeys>): UseZustandShallowSelectorReturns<T, TKeys> => {\n  const prev = useRef<Pick<T, TKeys>>({} as Pick<T, TKeys>);\n\n  return (state: T) => {\n    if (state) {\n      const next = pick(state, isArray(paths) ? paths : [paths]);\n      return shallow(prev.current, next) ? prev.current : (prev.current = next);\n    }\n    return prev.current;\n  };\n};\n\nexport default useZustandShallowSelector;\n"
  },
  {
    "path": "ui/src/i18n/index.ts",
    "content": "import { initReactI18next } from \"react-i18next\";\nimport i18n from \"i18next\";\nimport i18nBrowserLanguageDetector from \"i18next-browser-languagedetector\";\n\nimport resources, { LOCALE_EN_NAME, LOCALE_ZH_NAME } from \"./locales\";\n\ni18n\n  .use(i18nBrowserLanguageDetector)\n  .use(initReactI18next)\n  .init({\n    resources,\n    fallbackLng: LOCALE_EN_NAME,\n    debug: true,\n    interpolation: {\n      escapeValue: false,\n    },\n    detection: {\n      lookupLocalStorage: \"certimate-ui-lang\",\n    },\n  });\n\nexport const localeNames = {\n  ZH: LOCALE_ZH_NAME,\n  EN: LOCALE_EN_NAME,\n};\n\nexport const localeResources = resources;\n\nexport default i18n;\n"
  },
  {
    "path": "ui/src/i18n/locales/en/index.ts",
    "content": "﻿import nlsAccess from \"./nls.access.json\";\nimport nlsCertificate from \"./nls.certificate.json\";\nimport nlsCommon from \"./nls.common.json\";\nimport nlsDashboard from \"./nls.dashboard.json\";\nimport nlsLogin from \"./nls.login.json\";\nimport nlsPreset from \"./nls.preset.json\";\nimport nlsProvider from \"./nls.provider.json\";\nimport nlsSettings from \"./nls.settings.json\";\nimport nlsWorkflow from \"./nls.workflow.json\";\nimport nlsWorkflowNodes from \"./nls.workflow.nodes.json\";\nimport nlsWorkflowRuns from \"./nls.workflow.runs.json\";\nimport nlsWorkflowVars from \"./nls.workflow.vars.json\";\n\nexport default Object.freeze({\n  ...nlsCommon,\n  ...nlsLogin,\n  ...nlsDashboard,\n  ...nlsSettings,\n  ...nlsProvider,\n  ...nlsAccess,\n  ...nlsPreset,\n  ...nlsCertificate,\n  ...nlsWorkflow,\n  ...nlsWorkflowNodes,\n  ...nlsWorkflowRuns,\n  ...nlsWorkflowVars,\n});\n"
  },
  {
    "path": "ui/src/i18n/locales/en/nls.access.json",
    "content": "﻿{\r\n  \"access.page.title\": \"Credentials\",\r\n  \"access.page.subtitle\": \"Credentials store authentication information (username and password, API key, tokens, etc.) to connect with specific third-party apps and services.\",\r\n\r\n  \"access.nodata.title\": \"No credentials\",\r\n  \"access.nodata.description\": \"It looks like you don't have any credentials. Get started by adding one.\",\r\n  \"access.nodata.button\": \"Create credential\",\r\n\r\n  \"access.search.placeholder\": \"Search by credential name ...\",\r\n\r\n  \"access.action.create.button\": \"Create credential\",\r\n  \"access.action.create.modal.title\": \"Create credential\",\r\n  \"access.action.modify.menu\": \"Edit\",\r\n  \"access.action.modify.modal.title\": \"Edit credential\",\r\n  \"access.action.duplicate.menu\": \"Duplicate\",\r\n  \"access.action.duplicate.modal.title\": \"Duplicate credential\",\r\n  \"access.action.delete.menu\": \"Delete\",\r\n  \"access.action.delete.modal.title\": \"Delete \\\"{{name}}\\\"\",\r\n  \"access.action.delete.modal.content\": \"Are you sure want to delete this credential? <br>This action cannot be undone.\",\r\n  \"access.action.batch_delete.modal.title\": \"Delete credentials\",\r\n  \"access.action.batch_delete.modal.content\": \"Are you sure want to delete these {{count}} selected credentials? <br>This action cannot be undone.\",\r\n  \"access.action.test_notify.button\": \"Test notify\",\r\n\r\n  \"access.props.name\": \"Name\",\r\n  \"access.props.provider.usage.dns\": \"DNS\",\r\n  \"access.props.provider.usage.hosting\": \"Hosting\",\r\n  \"access.props.provider.usage.ca\": \"CA\",\r\n  \"access.props.provider.usage.notification\": \"Notification\",\r\n  \"access.props.provider.builtin\": \"Built-in\",\r\n  \"access.props.usage.dns_hosting\": \"Provider\",\r\n  \"access.props.usage.ca\": \"Certificate authority\",\r\n  \"access.props.usage.notification\": \"Notification channel\",\r\n  \"access.props.created_at\": \"Created at\",\r\n  \"access.props.updated_at\": \"Updated at\",\r\n\r\n  \"access.new.title\": \"Create Credential\",\r\n  \"access.new.subtitle\": \"Use this credential to connect with specific third-party apps and services.\",\r\n\r\n  \"access.form.name.label\": \"Name\",\r\n  \"access.form.name.placeholder\": \"Please enter credential name\",\r\n  \"access.form.provider.label\": \"Provider\",\r\n  \"access.form.provider.placeholder\": \"Please select a provider\",\r\n  \"access.form.provider.help\": \"DNS provider: The provider that hosts your domain names and manages your DNS records. <br>Hosting provider: The provider that hosts your servers or cloud services for deploying certificates.\",\r\n  \"access.form.provider.search.placeholder\": \"Search provider ...\",\r\n  \"access.form.shared_acme_eab_kid.label\": \"ACME EAB KID\",\r\n  \"access.form.shared_acme_eab_kid.placeholder\": \"Please enter ACME EAB KID\",\r\n  \"access.form.shared_acme_eab_hmac_key.label\": \"ACME EAB HMAC key\",\r\n  \"access.form.shared_acme_eab_hmac_key.placeholder\": \"Please enter ACME EAB HMAC key\",\r\n  \"access.form.shared_allow_insecure_conns.label\": \"Allow insecure SSL/TLS connections\",\r\n  \"access.form.1panel_server_url.label\": \"1Panel server URL\",\r\n  \"access.form.1panel_server_url.placeholder\": \"Please enter 1Panel server URL\",\r\n  \"access.form.1panel_server_url.help\": \"Notes: DO NOT include the security entrance suffix.\",\r\n  \"access.form.1panel_api_version.label\": \"1Panel version\",\r\n  \"access.form.1panel_api_version.placeholder\": \"Please select 1Panel version\",\r\n  \"access.form.1panel_api_key.label\": \"1Panel API key\",\r\n  \"access.form.1panel_api_key.placeholder\": \"Please enter 1Panel API key\",\r\n  \"access.form.1panel_api_key.tooltip\": \"For more information, see <a href=\\\"https://docs.1panel.pro/dev_manual/api_manual/\\\" target=\\\"_blank\\\">https://docs.1panel.pro/dev_manual/api_manual/</a>\",\r\n  \"access.form.35cn_username.label\": \"35.cn agent username\",\r\n  \"access.form.35cn_username.placeholder\": \"Please enter West.cn agent username\",\r\n  \"access.form.35cn_api_password.label\": \"35.cn agent API password\",\r\n  \"access.form.35cn_api_password.placeholder\": \"Please enter West.cn agent API password\",\r\n  \"access.form.35cn_agent.guide\": \"West.cn API only supports calls from agents. Learn more about this: <br><a href=\\\"https://console-docs.apipost.cn/preview/ab2c3103b22855ba/fac91d1e43fafb69?target_id=fa930623-3109-489d-9835-75fdfba07fbb\\\" target=\\\"_blank\\\">https://www.35.com/agent/mode-api.asp</a>\",\r\n  \"access.form.51dnscom_api_key.label\": \"51DNS.com API key\",\r\n  \"access.form.51dnscom_api_key.placeholder\": \"Please enter 51DNS.com API key\",\r\n  \"access.form.51dnscom_api_key.tooltip\": \"For more information, see <a href=\\\"https://www.51dns.com/member/apiSet\\\" target=\\\"_blank\\\">https://www.51dns.com/member/apiSet</a>\",\r\n  \"access.form.51dnscom_api_secret.label\": \"51DNS.com API secret\",\r\n  \"access.form.51dnscom_api_secret.placeholder\": \"Please enter 51DNS.com API secret\",\r\n  \"access.form.51dnscom_api_secret.tooltip\": \"For more information, see <a href=\\\"https://www.51dns.com/member/apiSet\\\" target=\\\"_blank\\\">https://www.51dns.com/member/apiSet</a>\",\r\n  \"access.form.acmeca_endpoint.label\": \"Endpoint\",\r\n  \"access.form.acmeca_endpoint.placeholder\": \"Please enter endpoint\",\r\n  \"access.form.acmeca_endpoint.tooltip\": \"For more information, see <a href=\\\"https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1\\\" target=\\\"_blank\\\">https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1</a>\",\r\n  \"access.form.acmeca_eab_kid.label\": \"ACME EAB KID (Optional)\",\r\n  \"access.form.acmeca_eab_kid.placeholder\": \"Please enter ACME EAB KID\",\r\n  \"access.form.acmeca_eab_hmac_key.label\": \"ACME EAB HMAC key (Optional)\",\r\n  \"access.form.acmeca_eab_hmac_key.placeholder\": \"Please enter ACME EAB HMAC key\",\r\n  \"access.form.acmedns_server_url.label\": \"ACME-DNS server URL\",\r\n  \"access.form.acmedns_server_url.placeholder\": \"Please enter ACME-DNS server URL\",\r\n  \"access.form.acmedns_credentials.label\": \"ACME-DNS credentials\",\r\n  \"access.form.acmedns_credentials.placeholder\": \"Please enter ACME-DNS credentials\",\r\n  \"access.form.acmedns_credentials.tooltip\": \"For more information, see <a href=\\\"https://github.com/joohoi/acme-dns\\\" target=\\\"_blank\\\">https://github.com/joohoi/acme-dns</a>\",\r\n  \"access.form.acmehttpreq_endpoint.label\": \"Endpoint\",\r\n  \"access.form.acmehttpreq_endpoint.placeholder\": \"Please enter endpoint\",\r\n  \"access.form.acmehttpreq_endpoint.tooltip\": \"For more information, see <a href=\\\"https://go-acme.github.io/lego/dns/httpreq/\\\" target=\\\"_blank\\\">https://go-acme.github.io/lego/dns/httpreq/</a>\",\r\n  \"access.form.acmehttpreq_mode.label\": \"Mode\",\r\n  \"access.form.acmehttpreq_mode.placeholder\": \"Please select mode\",\r\n  \"access.form.acmehttpreq_mode.tooltip\": \"For more information, see <a href=\\\"https://go-acme.github.io/lego/dns/httpreq/\\\" target=\\\"_blank\\\">https://go-acme.github.io/lego/dns/httpreq/</a>\",\r\n  \"access.form.acmehttpreq_username.label\": \"HTTP Basic Auth username (Optional)\",\r\n  \"access.form.acmehttpreq_username.placeholder\": \"Please enter HTTP Basic Auth username\",\r\n  \"access.form.acmehttpreq_username.tooltip\": \"For more information, see <a href=\\\"https://go-acme.github.io/lego/dns/httpreq/\\\" target=\\\"_blank\\\">https://go-acme.github.io/lego/dns/httpreq/</a>\",\r\n  \"access.form.acmehttpreq_password.label\": \"HTTP Basic Auth password (Optional)\",\r\n  \"access.form.acmehttpreq_password.placeholder\": \"Please enter HTTP Basic Auth password\",\r\n  \"access.form.acmehttpreq_password.tooltip\": \"For more information, see <a href=\\\"https://go-acme.github.io/lego/dns/httpreq/\\\" target=\\\"_blank\\\">https://go-acme.github.io/lego/dns/httpreq/</a>\",\r\n  \"access.form.actalisssl_eab.guide\": \"Learn more about using EAB key in Actalis SSL: <br><a href=\\\"https://www.actalis.com/manage-with-acme\\\" target=\\\"_blank\\\">https://www.actalis.com/manage-with-acme</a>\",\r\n  \"access.form.akamai_host.label\": \"Akamai API host\",\r\n  \"access.form.akamai_host.placeholder\": \"Please enter Akamai API host\",\r\n  \"access.form.akamai_host.tooltip\": \"For more information, see <a href=\\\"https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials\\\" target=\\\"_blank\\\">https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials</a>\",\r\n  \"access.form.akamai_client_token.label\": \"Akamai client token\",\r\n  \"access.form.akamai_client_token.placeholder\": \"Please enter Akamai client token\",\r\n  \"access.form.akamai_client_token.tooltip\": \"For more information, see <a href=\\\"https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials\\\" target=\\\"_blank\\\">https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials</a>\",\r\n  \"access.form.akamai_client_secret.label\": \"Akamai client secret\",\r\n  \"access.form.akamai_client_secret.placeholder\": \"Please enter Akamai client secret\",\r\n  \"access.form.akamai_client_secret.tooltip\": \"For more information, see <a href=\\\"https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials\\\" target=\\\"_blank\\\">https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials</a>\",\r\n  \"access.form.akamai_access_token.label\": \"Akamai access token\",\r\n  \"access.form.akamai_access_token.placeholder\": \"Please enter Akamai access token\",\r\n  \"access.form.akamai_access_token.tooltip\": \"For more information, see <a href=\\\"https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials\\\" target=\\\"_blank\\\">https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials</a>\",\r\n  \"access.form.aliyun_access_key_id.label\": \"Aliyun AccessKeyID\",\r\n  \"access.form.aliyun_access_key_id.placeholder\": \"Please enter Aliyun AccessKeyID\",\r\n  \"access.form.aliyun_access_key_id.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/acr/create-and-obtain-an-accesskey-pair\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/acr/create-and-obtain-an-accesskey-pair</a>\",\r\n  \"access.form.aliyun_access_key_secret.label\": \"Aliyun AccessKeySecret\",\r\n  \"access.form.aliyun_access_key_secret.placeholder\": \"Please enter Aliyun AccessKeySecret\",\r\n  \"access.form.aliyun_access_key_secret.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/acr/create-and-obtain-an-accesskey-pair\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/acr/create-and-obtain-an-accesskey-pair</a>\",\r\n  \"access.form.aliyun_resource_group_id.label\": \"Aliyun resource group ID (Optional)\",\r\n  \"access.form.aliyun_resource_group_id.placeholder\": \"Please enter Aliyun resource group ID\",\r\n  \"access.form.aliyun_resource_group_id.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/resource-management/product-overview\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/resource-management/product-overview</a>\",\r\n  \"access.form.apisix_server_url.label\": \"APISIX server URL\",\r\n  \"access.form.apisix_server_url.placeholder\": \"Please enter APISIX server URL\",\r\n  \"access.form.apisix_api_key.label\": \"APISIX Admin API key\",\r\n  \"access.form.apisix_api_key.placeholder\": \"Please enter APISIX Admin API key\",\r\n  \"access.form.apisix_api_key.tooltip\": \"For more information, see <a href=\\\"https://apisix.apache.org/docs/apisix/admin-api/\\\" target=\\\"_blank\\\">https://apisix.apache.org/docs/apisix/admin-api/</a>\",\r\n  \"access.form.arvancloud_api_key.label\": \"ArvanCloud API key\",\r\n  \"access.form.arvancloud_api_key.placeholder\": \"Please enter ArvanCloud API key\",\r\n  \"access.form.arvancloud_api_key.tooltip\": \"For more information, see <a href=\\\"https://docs.arvancloud.ir/en/developer-tools/api/api-key\\\" target=\\\"_blank\\\">https://docs.arvancloud.ir/en/developer-tools/api/api-key</a>\",\r\n  \"access.form.aws_access_key_id.label\": \"AWS AccessKeyID\",\r\n  \"access.form.aws_access_key_id.placeholder\": \"Please enter AWS AccessKeyID\",\r\n  \"access.form.aws_access_key_id.tooltip\": \"For more information, see <a href=\\\"https://docs.aws.amazon.com/en_us/IAM/latest/UserGuide/id_credentials_access-keys.html\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/en_us/IAM/latest/UserGuide/id_credentials_access-keys.html</a>\",\r\n  \"access.form.aws_secret_access_key.label\": \"AWS SecretAccessKey\",\r\n  \"access.form.aws_secret_access_key.placeholder\": \"Please enter AWS SecretAccessKey\",\r\n  \"access.form.aws_secret_access_key.tooltip\": \"For more information, see <a href=\\\"https://docs.aws.amazon.com/en_us/IAM/latest/UserGuide/id_credentials_access-keys.html\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/en_us/IAM/latest/UserGuide/id_credentials_access-keys.html</a>\",\r\n  \"access.form.azure_tenant_id.label\": \"Azure tenant ID\",\r\n  \"access.form.azure_tenant_id.placeholder\": \"Please enter Azure tenant ID\",\r\n  \"access.form.azure_tenant_id.tooltip\": \"For more information, see <a href=\\\"https://learn.microsoft.com/en-us/azure/azure-portal/get-subscription-tenant-id\\\" target=\\\"_blank\\\">https://learn.microsoft.com/en-us/azure/azure-portal/get-subscription-tenant-id</a>\",\r\n  \"access.form.azure_client_id.label\": \"Azure client ID\",\r\n  \"access.form.azure_client_id.placeholder\": \"Please enter Azure client ID\",\r\n  \"access.form.azure_client_id.tooltip\": \"For more information, see <a href=\\\"https://learn.microsoft.com/en-us/azure/azure-monitor/logs/api/register-app-for-token\\\" target=\\\"_blank\\\">https://learn.microsoft.com/en-us/azure/azure-monitor/logs/api/register-app-for-token</a>\",\r\n  \"access.form.azure_client_secret.label\": \"Azure client secret\",\r\n  \"access.form.azure_client_secret.placeholder\": \"Please enter Azure client secret\",\r\n  \"access.form.azure_client_secret.tooltip\": \"For more information, see <a href=\\\"https://learn.microsoft.com/en-us/azure/azure-monitor/logs/api/register-app-for-token\\\" target=\\\"_blank\\\">https://learn.microsoft.com/en-us/azure/azure-monitor/logs/api/register-app-for-token</a>\",\r\n  \"access.form.azure_subscription_id.label\": \"Azure subscription ID (Optional)\",\r\n  \"access.form.azure_subscription_id.placeholder\": \"Please enter Azure subscription ID\",\r\n  \"access.form.azure_subscription_id.tooltip\": \"For more information, see <a href=\\\"https://learn.microsoft.com/en-us/azure/azure-portal/get-subscription-tenant-id\\\" target=\\\"_blank\\\">https://learn.microsoft.com/en-us/azure/azure-portal/get-subscription-tenant-id</a>\",\r\n  \"access.form.azure_resource_group_name.label\": \"Azure resource group name (Optional)\",\r\n  \"access.form.azure_resource_group_name.placeholder\": \"Please enter Azure resource group name\",\r\n  \"access.form.azure_resource_group_name.tooltip\": \"For more information, see <a href=\\\"https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-portal\\\" target=\\\"_blank\\\">https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-portal</a>\",\r\n  \"access.form.azure_cloud_name.label\": \"Azure sovereign cloud name (Optional)\",\r\n  \"access.form.azure_cloud_name.placeholder\": \"Please enter Azure sovereign cloud name (e.g. public)\",\r\n  \"access.form.azure_cloud_name.tooltip\": \"For more information, see <a href=\\\"https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/sovereign-clouds\\\" target=\\\"_blank\\\">https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/sovereign-clouds</a>\",\r\n  \"access.form.baiducloud_access_key_id.label\": \"Baidu Cloud AccessKeyID\",\r\n  \"access.form.baiducloud_access_key_id.placeholder\": \"Please enter Baidu Cloud AccessKeyID\",\r\n  \"access.form.baiducloud_access_key_id.tooltip\": \"For more information, see <a href=\\\"https://intl.cloud.baidu.com/doc/Reference/s/jjwvz2e3p-en\\\" target=\\\"_blank\\\">https://intl.cloud.baidu.com/doc/Reference/s/jjwvz2e3p-en</a>\",\r\n  \"access.form.baiducloud_secret_access_key.label\": \"Baidu Cloud SecretAccessKey\",\r\n  \"access.form.baiducloud_secret_access_key.placeholder\": \"Please enter Baidu Cloud SecretAccessKey\",\r\n  \"access.form.baiducloud_secret_access_key.tooltip\": \"For more information, see <a href=\\\"https://intl.cloud.baidu.com/doc/Reference/s/jjwvz2e3p-en\\\" target=\\\"_blank\\\">https://intl.cloud.baidu.com/doc/Reference/s/jjwvz2e3p-en</a>\",\r\n  \"access.form.baishan_api_token.label\": \"Baishan Cloud API token\",\r\n  \"access.form.baishan_api_token.placeholder\": \"Please enter Baishan Cloud API token\",\r\n  \"access.form.baotapanel_server_url.label\": \"aaPanel server URL\",\r\n  \"access.form.baotapanel_server_url.placeholder\": \"Please enter aaPanel server URL\",\r\n  \"access.form.baotapanel_server_url.help\": \"Notes: DO NOT include the security entrance suffix.\",\r\n  \"access.form.baotapanel_api_key.label\": \"aaPanel API key\",\r\n  \"access.form.baotapanel_api_key.placeholder\": \"Please enter aaPanel API key\",\r\n  \"access.form.baotapanel_api_key.tooltip\": \"For more information, see <a href=\\\"https://www.bt.cn/bbs/thread-20376-1-1.html\\\" target=\\\"_blank\\\">https://www.bt.cn/bbs/thread-20376-1-1.html</a>\",\r\n  \"access.form.baotapanelgo_server_url.label\": \"aaPanel WinGo server URL\",\r\n  \"access.form.baotapanelgo_server_url.placeholder\": \"Please enter aaPanel WinGo server URL\",\r\n  \"access.form.baotapanelgo_server_url.help\": \"Notes: DO NOT include the security entrance suffix.\",\r\n  \"access.form.baotapanelgo_api_key.label\": \"aaPanel WinGo API key\",\r\n  \"access.form.baotapanelgo_api_key.placeholder\": \"Please enter aaPanel WinGo API key\",\r\n  \"access.form.baotapanelgo_api_key.tooltip\": \"For more information, see <a href=\\\"https://www.bt.cn/bbs/thread-20376-1-1.html\\\" target=\\\"_blank\\\">https://www.bt.cn/bbs/thread-20376-1-1.html</a>\",\r\n  \"access.form.baotawaf_server_url.label\": \"aaWAF server URL\",\r\n  \"access.form.baotawaf_server_url.placeholder\": \"Please enter aaWAF server URL\",\r\n  \"access.form.baotawaf_server_url.help\": \"Notes: DO NOT include the security entrance suffix.\",\r\n  \"access.form.baotawaf_api_key.label\": \"aaWAF API key\",\r\n  \"access.form.baotawaf_api_key.placeholder\": \"Please enter aaWAF API key\",\r\n  \"access.form.baotawaf_api_key.tooltip\": \"For more information, see <a href=\\\"https://github.com/aaPanel/aaWAF/blob/main/API.md\\\" target=\\\"_blank\\\">https://github.com/aaPanel/aaWAF/blob/main/API.md</a>\",\r\n  \"access.form.bookmyname_username.label\": \"BookMyName username\",\r\n  \"access.form.bookmyname_username.placeholder\": \"Please enter BookMyName username\",\r\n  \"access.form.bookmyname_password.label\": \"BookMyName password\",\r\n  \"access.form.bookmyname_password.placeholder\": \"Please enter BookMyName password\",\r\n  \"access.form.bunny_api_key.label\": \"Bunny API key\",\r\n  \"access.form.bunny_api_key.placeholder\": \"Please enter Bunny API key\",\r\n  \"access.form.bunny_api_key.tooltip\": \"For more information, see <a href=\\\"https://docs.bunny.net/reference/bunnynet-api-overview\\\" target=\\\"_blank\\\">https://docs.bunny.net/reference/bunnynet-api-overview</a>\",\r\n  \"access.form.byteplus_access_key.label\": \"BytePlus AccessKey\",\r\n  \"access.form.byteplus_access_key.placeholder\": \"Please enter BytePlus AccessKey\",\r\n  \"access.form.byteplus_access_key.tooltip\": \"For more information, see <a href=\\\"https://docs.byteplus.com/en/docs/byteplus-platform/docs-managing-keys\\\" target=\\\"_blank\\\">https://docs.byteplus.com/en/docs/byteplus-platform/docs-managing-keys</a>\",\r\n  \"access.form.byteplus_secret_key.label\": \"BytePlus SecretKey\",\r\n  \"access.form.byteplus_secret_key.placeholder\": \"Please enter BytePlus SecretKey\",\r\n  \"access.form.byteplus_secret_key.tooltip\": \"For more information, see <a href=\\\"https://docs.byteplus.com/en/docs/byteplus-platform/docs-managing-keys\\\" target=\\\"_blank\\\">https://docs.byteplus.com/en/docs/byteplus-platform/docs-managing-keys</a>\",\r\n  \"access.form.cachefly_api_token.label\": \"CacheFly API token\",\r\n  \"access.form.cachefly_api_token.placeholder\": \"Please enter CacheFly API token\",\r\n  \"access.form.cachefly_api_token.tooltip\": \"For more information, see <a href=\\\"https://kb.cachefly.com/kb/guide/en/generating-tokens-and-keys-Oll9Irt5TI/Steps/2460228\\\" target=\\\"_blank\\\">https://kb.cachefly.com/kb/guide/en/generating-tokens-and-keys-Oll9Irt5TI/Steps/2460228</a>\",\r\n  \"access.form.cdnfly_server_url.label\": \"Cdnfly server URL\",\r\n  \"access.form.cdnfly_server_url.placeholder\": \"Please enter Cdnfly server URL\",\r\n  \"access.form.cdnfly_api_key.label\": \"Cdnfly user API key\",\r\n  \"access.form.cdnfly_api_key.placeholder\": \"Please enter Cdnfly user API key\",\r\n  \"access.form.cdnfly_api_key.tooltip\": \"For more information, see <a href=\\\"https://doc.cdnfly.cn/shiyongjieshao.html\\\" target=\\\"_blank\\\">https://doc.cdnfly.cn/shiyongjieshao.html</a>\",\r\n  \"access.form.cdnfly_api_secret.label\": \"Cdnfly user API secret\",\r\n  \"access.form.cdnfly_api_secret.placeholder\": \"Please enter Cdnfly user API secret\",\r\n  \"access.form.cdnfly_api_secret.tooltip\": \"For more information, see <a href=\\\"https://doc.cdnfly.cn/shiyongjieshao.html\\\" target=\\\"_blank\\\">https://doc.cdnfly.cn/shiyongjieshao.html</a>\",\r\n  \"access.form.cloudflare_dns_api_token.label\": \"Cloudflare DNS API token\",\r\n  \"access.form.cloudflare_dns_api_token.placeholder\": \"Please enter Cloudflare DNS API token\",\r\n  \"access.form.cloudflare_dns_api_token.tooltip\": \"For more information, see <a href=\\\"https://developers.cloudflare.com/fundamentals/api/get-started/create-token/\\\" target=\\\"_blank\\\">https://developers.cloudflare.com/fundamentals/api/get-started/create-token/</a>\",\r\n  \"access.form.cloudflare_zone_api_token.label\": \"Cloudflare Zone API token (Optional)\",\r\n  \"access.form.cloudflare_zone_api_token.placeholder\": \"Please enter Cloudflare Zone API token\",\r\n  \"access.form.cloudflare_zone_api_token.help\": \"Notes: Only required when you scope the DNS API token to <b>specific zones</b>. PLease scope the Zone API token to <b>all zones</b> with <i>Zone/Zone/Read</i> permission.\",\r\n  \"access.form.cloudflare_zone_api_token.tooltip\": \"For more information, see <a href=\\\"https://developers.cloudflare.com/fundamentals/api/get-started/create-token/\\\" target=\\\"_blank\\\">https://developers.cloudflare.com/fundamentals/api/get-started/create-token/</a>\",\r\n  \"access.form.cloudns_auth_id.label\": \"ClouDNS API user ID\",\r\n  \"access.form.cloudns_auth_id.placeholder\": \"Please enter ClouDNS API user ID\",\r\n  \"access.form.cloudns_auth_id.tooltip\": \"For more information, see <a href=\\\"https://www.cloudns.net/wiki/article/42/\\\" target=\\\"_blank\\\">https://www.cloudns.net/wiki/article/42/</a>\",\r\n  \"access.form.cloudns_auth_password.label\": \"ClouDNS API user password\",\r\n  \"access.form.cloudns_auth_password.placeholder\": \"Please enter ClouDNS API user password\",\r\n  \"access.form.cloudns_auth_password.tooltip\": \"For more information, see <a href=\\\"https://www.cloudns.net/wiki/article/42/\\\" target=\\\"_blank\\\">https://www.cloudns.net/wiki/article/42/</a>\",\r\n  \"access.form.cmcccloud_access_key_id.label\": \"CMCC ECloud AccessKeyID\",\r\n  \"access.form.cmcccloud_access_key_id.placeholder\": \"Please enter CMCC ECloud AccessKeyID\",\r\n  \"access.form.cmcccloud_access_key_id.tooltip\": \"For more information, see <a href=\\\"https://ecloud.10086.cn/op-help-center/doc/article/49739\\\" target=\\\"_blank\\\">https://ecloud.10086.cn/op-help-center/doc/article/49739</a>\",\r\n  \"access.form.cmcccloud_access_key_secret.label\": \"CMCC ECloud AccessKeySecret\",\r\n  \"access.form.cmcccloud_access_key_secret.placeholder\": \"Please enter CMCC ECloud AccessKeySecret\",\r\n  \"access.form.cmcccloud_access_key_secret.tooltip\": \"For more information, see <a href=\\\"https://ecloud.10086.cn/op-help-center/doc/article/49739\\\" target=\\\"_blank\\\">https://ecloud.10086.cn/op-help-center/doc/article/49739</a>\",\r\n  \"access.form.constellix_api_key.label\": \"Constellix API key\",\r\n  \"access.form.constellix_api_key.placeholder\": \"Please enter Constellix API key\",\r\n  \"access.form.constellix_api_key.tooltip\": \"For more information, see <a href=\\\"https://support.constellix.com/hc/en-us/articles/34574197390491-How-to-Generate-an-API-Key\\\" target=\\\"_blank\\\">https://support.constellix.com/hc/en-us/articles/34574197390491-How-to-Generate-an-API-Key</a>\",\r\n  \"access.form.constellix_secret_key.label\": \"Constellix API secret key\",\r\n  \"access.form.constellix_secret_key.placeholder\": \"Please enter Constellix API secret key\",\r\n  \"access.form.constellix_secret_key.tooltip\": \"For more information, see <a href=\\\"https://support.constellix.com/hc/en-us/articles/34574197390491-How-to-Generate-an-API-Key\\\" target=\\\"_blank\\\">https://support.constellix.com/hc/en-us/articles/34574197390491-How-to-Generate-an-API-Key</a>\",\r\n  \"access.form.cpanel_server_url.label\": \"cPanel server URL\",\r\n  \"access.form.cpanel_server_url.placeholder\": \"Please enter cPanel server URL\",\r\n  \"access.form.cpanel_username.label\": \"cPanel username\",\r\n  \"access.form.cpanel_username.placeholder\": \"Please enter cPanel username\",\r\n  \"access.form.cpanel_api_token.label\": \"cPanel API token\",\r\n  \"access.form.cpanel_api_token.placeholder\": \"Please enter cPanel API token\",\r\n  \"access.form.cpanel_api_token.tooltip\": \"For more information, see <a href=\\\"https://docs.cpanel.net/cpanel/security/manage-api-tokens-in-cpanel/\\\" target=\\\"_blank\\\">https://docs.cpanel.net/cpanel/security/manage-api-tokens-in-cpanel/</a>\",\r\n  \"access.form.ctcccloud_access_key_id.label\": \"CTCC StateCloud AccessKeyID\",\r\n  \"access.form.ctcccloud_access_key_id.placeholder\": \"Please enter CTCC StateCloud AccessKeyID\",\r\n  \"access.form.ctcccloud_access_key_id.tooltip\": \"For more information, see <a href=\\\"https://www.ctyun.cn/document/10015882/10015953\\\" target=\\\"_blank\\\">https://www.ctyun.cn/document/10015882/10015953</a>\",\r\n  \"access.form.ctcccloud_secret_access_key.label\": \"CTCC StateCloud SecretAccessKey\",\r\n  \"access.form.ctcccloud_secret_access_key.placeholder\": \"Please enter CTCC StateCloud SecretAccessKey\",\r\n  \"access.form.ctcccloud_secret_access_key.tooltip\": \"For more information, see <a href=\\\"https://www.ctyun.cn/document/10015882/10015953\\\" target=\\\"_blank\\\">https://www.ctyun.cn/document/10015882/10015953</a>\",\r\n  \"access.form.desec_token.label\": \"deSEC token\",\r\n  \"access.form.desec_token.placeholder\": \"Please enter deSEC token\",\r\n  \"access.form.desec_token.tooltip\": \"For more information, see <a href=\\\"https://desec.readthedocs.io/en/latest/auth/tokens.html#manage-tokens\\\" target=\\\"_blank\\\">https://desec.readthedocs.io/en/latest/auth/tokens.html</a>\",\r\n  \"access.form.digicert_eab.guide\": \"Learn more about using EAB key in DigiCert: <br><a href=\\\"https://docs.digicert.com/en/certcentral/certificate-tools/certificate-lifecycle-automation-guides/third-party-acme-integration/use-legacy-certcentral-acme-credentials.html\\\" target=\\\"_blank\\\">https://docs.digicert.com/en/certcentral/certificate-tools/certificate-lifecycle-automation-guides/third-party-acme-integration/use-legacy-certcentral-acme-credentials.html</a>\",\r\n  \"access.form.digitalocean_access_token.label\": \"DigitalOcean access token\",\r\n  \"access.form.digitalocean_access_token.placeholder\": \"Please enter DigitalOcean access token\",\r\n  \"access.form.digitalocean_access_token.tooltip\": \"For more information, see <a href=\\\"https://docs.digitalocean.com/reference/api/create-personal-access-token/\\\" target=\\\"_blank\\\">https://docs.digitalocean.com/reference/api/create-personal-access-token/</a>\",\r\n  \"access.form.dingtalkbot_webhook_url.label\": \"DingTalk bot Webhook URL\",\r\n  \"access.form.dingtalkbot_webhook_url.placeholder\": \"Please enter DingTalk bot Webhook URL\",\r\n  \"access.form.dingtalkbot_webhook_url.tooltip\": \"For more information, see <a href=\\\"https://open.dingtalk.com/document/orgapp/obtain-the-webhook-address-of-a-custom-robot\\\" target=\\\"_blank\\\">https://open.dingtalk.com/document/orgapp/obtain-the-webhook-address-of-a-custom-robot</a>\",\r\n  \"access.form.dingtalkbot_secret.label\": \"DingTalk bot secret\",\r\n  \"access.form.dingtalkbot_secret.placeholder\": \"Please enter DingTalk bot secret\",\r\n  \"access.form.dingtalkbot_secret.tooltip\": \"For more information, see <a href=\\\"https://open.dingtalk.com/document/orgapp/customize-robot-security-settings\\\" target=\\\"_blank\\\">https://open.dingtalk.com/document/orgapp/customize-robot-security-settings</a>\",\r\n  \"access.form.dingtalkbot_custom_payload.label\": \"DingTalk bot message payload (Optional)\",\r\n  \"access.form.dingtalkbot_custom_payload.placeholder\": \"Please enter custom DingTalk bot message payload\",\r\n  \"access.form.dingtalkbot_custom_payload.checkbox\": \"Customized message payload\",\r\n  \"access.form.discordbot_token.label\": \"Discord bot token\",\r\n  \"access.form.discordbot_token.placeholder\": \"Please enter Discord bot token\",\r\n  \"access.form.discordbot_token.tooltip\": \"For more information, see <a href=\\\"https://docs.discordbotstudio.org/setting-up-dbs/finding-your-bot-token\\\" target=\\\"_blank\\\">https://docs.discordbotstudio.org/setting-up-dbs/finding-your-bot-token</a>\",\r\n  \"access.form.discordbot_channel_id.label\": \"Discord channel ID (Optional)\",\r\n  \"access.form.discordbot_channel_id.placeholder\": \"Please enter the default Discord channel ID\",\r\n  \"access.form.discordbot_channel_id.help\": \"Notes: It can be overrided in the workflows.\",\r\n  \"access.form.discordbot_channel_id.tooltip\": \"For more information, see <a href=\\\"https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID\\\" target=\\\"_blank\\\">https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID</a>\",\r\n  \"access.form.dnsexit_api_key.label\": \"DNSExit API key\",\r\n  \"access.form.dnsexit_api_key.placeholder\": \"Please enter DNSExit API key\",\r\n  \"access.form.dnsexit_api_key.tooltip\": \"For more information, see <a href=\\\"https://dnsexit.com/Direct.sv?cmd=userApiKey\\\" target=\\\"_blank\\\">https://dnsexit.com/Direct.sv?cmd=userApiKey</a>\",\r\n  \"access.form.dnsla_api_id.label\": \"DNS.LA API ID\",\r\n  \"access.form.dnsla_api_id.placeholder\": \"Please enter DNS.LA API ID\",\r\n  \"access.form.dnsla_api_id.tooltip\": \"For more information, see <a href=\\\"https://www.dns.la/docs/ApiDoc\\\" target=\\\"_blank\\\">https://www.dns.la/docs/ApiDoc</a>\",\r\n  \"access.form.dnsla_api_secret.label\": \"DNS.LA API secret\",\r\n  \"access.form.dnsla_api_secret.placeholder\": \"Please enter DNS.LA API secret\",\r\n  \"access.form.dnsla_api_secret.tooltip\": \"For more information, see <a href=\\\"https://www.dns.la/docs/ApiDoc\\\" target=\\\"_blank\\\">https://www.dns.la/docs/ApiDoc</a>\",\r\n  \"access.form.dnsmadeeasy_api_key.label\": \"DNS Made Easy API key\",\r\n  \"access.form.dnsmadeeasy_api_key.placeholder\": \"Please enter DNS Made Easy API key\",\r\n  \"access.form.dnsmadeeasy_api_key.tooltip\": \"For more information, see <a href=\\\"https://api-docs.dnsmadeeasy.com/#5b98221f-37e9-4845-a349-5e959241b4a5\\\" target=\\\"_blank\\\">https://api-docs.dnsmadeeasy.com/#Authentication</a>\",\r\n  \"access.form.dnsmadeeasy_api_secret.label\": \"DNS Made Easy API secret\",\r\n  \"access.form.dnsmadeeasy_api_secret.placeholder\": \"Please enter DNS Made Easy API secret\",\r\n  \"access.form.dnsmadeeasy_api_secret.tooltip\": \"For more information, see <a href=\\\"https://api-docs.dnsmadeeasy.com/#5b98221f-37e9-4845-a349-5e959241b4a5\\\" target=\\\"_blank\\\">https://api-docs.dnsmadeeasy.com/#Authentication</a>\",\r\n  \"access.form.dogecloud_access_key.label\": \"Doge Cloud AccessKey\",\r\n  \"access.form.dogecloud_access_key.placeholder\": \"Please enter Doge Cloud AccessKey\",\r\n  \"access.form.dogecloud_access_key.tooltip\": \"For more information, see <a href=\\\"https://console.dogecloud.com/\\\" target=\\\"_blank\\\">https://console.dogecloud.com/</a>\",\r\n  \"access.form.dogecloud_secret_key.label\": \"Doge Cloud SecretKey\",\r\n  \"access.form.dogecloud_secret_key.placeholder\": \"Please enter Doge Cloud SecretKey\",\r\n  \"access.form.dogecloud_secret_key.tooltip\": \"For more information, see <a href=\\\"https://console.dogecloud.com/\\\" target=\\\"_blank\\\">https://console.dogecloud.com/</a>\",\r\n  \"access.form.dokploy_server_url.label\": \"Dokploy server URL\",\r\n  \"access.form.dokploy_server_url.placeholder\": \"Please enter Dokploy server URL\",\r\n  \"access.form.dokploy_api_key.label\": \"Dokploy API key\",\r\n  \"access.form.dokploy_api_key.placeholder\": \"Please enter Dokploy API key\",\r\n  \"access.form.dokploy_api_key.tooltip\": \"For more information, see <a href=\\\"https://docs.dokploy.com/docs/api\\\" target=\\\"_blank\\\">https://docs.dokploy.com/docs/api</a>\",\r\n  \"access.form.duckdns_token.label\": \"DuckDNS token\",\r\n  \"access.form.duckdns_token.placeholder\": \"Please enter DuckDNS token\",\r\n  \"access.form.duckdns_token.tooltip\": \"For more information, see <a href=\\\"https://www.duckdns.org/spec.jsp\\\" target=\\\"_blank\\\">https://www.duckdns.org/spec.jsp</a>\",\r\n  \"access.form.dynu_api_key.label\": \"Dynu API key\",\r\n  \"access.form.dynu_api_key.placeholder\": \"Please enter Dynu API key\",\r\n  \"access.form.dynu_api_key.tooltip\": \"For more information, see <a href=\\\"https://www.dynu.com/Support/API#Authentication\\\" target=\\\"_blank\\\">https://www.dynu.com/Support/API#Authentication</a>\",\r\n  \"access.form.dynv6_http_token.label\": \"dynv6 HTTP token\",\r\n  \"access.form.dynv6_http_token.placeholder\": \"Please enter dynv6 HTTP token\",\r\n  \"access.form.dynv6_http_token.tooltip\": \"For more information, see <a href=\\\"https://dynv6.com/keys\\\" target=\\\"_blank\\\">https://dynv6.com/keys</a>\",\r\n  \"access.form.email_smtp_host.label\": \"SMTP host\",\r\n  \"access.form.email_smtp_host.placeholder\": \"Please enter SMTP host\",\r\n  \"access.form.email_smtp_port.label\": \"SMTP port\",\r\n  \"access.form.email_smtp_port.placeholder\": \"Please enter SMTP port\",\r\n  \"access.form.email_smtp_tls.label\": \"Security connection\",\r\n  \"access.form.email_smtp_tls.placeholder\": \"Please select security connection\",\r\n  \"access.form.email_smtp_tls.option.true.label\": \"Force SSL/TLS connection\",\r\n  \"access.form.email_smtp_tls.option.false.label\": \"Prefer STARTTLS, fallback to plain if failed\",\r\n  \"access.form.email_username.label\": \"Username\",\r\n  \"access.form.email_username.placeholder\": \"please enter username\",\r\n  \"access.form.email_password.label\": \"Password\",\r\n  \"access.form.email_password.placeholder\": \"please enter password\",\r\n  \"access.form.email_sender_address.label\": \"Sender email address\",\r\n  \"access.form.email_sender_address.placeholder\": \"Please enter sender email address\",\r\n  \"access.form.email_sender_name.label\": \"Sender display name (Optional)\",\r\n  \"access.form.email_sender_name.placeholder\": \"Please enter sender display name\",\r\n  \"access.form.email_receiver_address.label\": \"Receiver email address (Optional)\",\r\n  \"access.form.email_receiver_address.placeholder\": \"Please enter the default receiver email address\",\r\n  \"access.form.email_receiver_address.help\": \"Notes: It can be overrided in the workflows.\",\r\n  \"access.form.flexcdn_server_url.label\": \"FlexCDN server URL\",\r\n  \"access.form.flexcdn_server_url.placeholder\": \"Please enter FlexCDN server URL\",\r\n  \"access.form.flexcdn_api_role.label\": \"FlexCDN user role\",\r\n  \"access.form.flexcdn_api_role.placeholder\": \"Please select FlexCDN user role\",\r\n  \"access.form.flexcdn_api_role.option.user.label\": \"Platform user\",\r\n  \"access.form.flexcdn_api_role.option.admin.label\": \"Administrator user\",\r\n  \"access.form.flexcdn_access_key_id.label\": \"FlexCDN AccessKeyID\",\r\n  \"access.form.flexcdn_access_key_id.placeholder\": \"Please enter FlexCDN AccessKeyID\",\r\n  \"access.form.flexcdn_access_key_id.tooltip\": \"For more information, see <a href=\\\"https://flexcdn.cn/docs/api/auth\\\" target=\\\"_blank\\\">https://flexcdn.cn/docs/api/auth</a>\",\r\n  \"access.form.flexcdn_access_key.label\": \"FlexCDN AccessKey\",\r\n  \"access.form.flexcdn_access_key.placeholder\": \"Please enter FlexCDN AccessKey\",\r\n  \"access.form.flexcdn_access_key.tooltip\": \"For more information, see <a href=\\\"https://flexcdn.cn/docs/api/auth\\\" target=\\\"_blank\\\">https://flexcdn.cn/docs/api/auth</a>\",\r\n  \"access.form.flyio_api_token.label\": \"Fly.io API token\",\r\n  \"access.form.flyio_api_token.placeholder\": \"Please enter Fly.io API token\",\r\n  \"access.form.flyio_api_token.tooltip\": \"For more information, see <a href=\\\"https://fly.io/docs/security/tokens/\\\" target=\\\"_blank\\\">https://fly.io/docs/security/tokens/</a>\",\r\n  \"access.form.gandinet_personal_access_token.label\": \"Gandi.net personal access token\",\r\n  \"access.form.gandinet_personal_access_token.placeholder\": \"Please enter Gandi.net personal access token\",\r\n  \"access.form.gandinet_personal_access_token.tooltip\": \"For more information, see <a href=\\\"https://api.gandi.net/docs/authentication/\\\" target=\\\"_blank\\\">https://api.gandi.net/docs/authentication/</a>\",\r\n  \"access.form.gcore_api_token.label\": \"G-Core API token\",\r\n  \"access.form.gcore_api_token.placeholder\": \"Please enter G-Core API token\",\r\n  \"access.form.gcore_api_token.tooltip\": \"For more information, see <a href=\\\"https://api.gcore.com/docs/iam#section/Authentication\\\" target=\\\"_blank\\\">https://api.gcore.com/docs/iam#section/Authentication</a>\",\r\n  \"access.form.gname_app_id.label\": \"GNAME AppID\",\r\n  \"access.form.gname_app_id.placeholder\": \"Please enter GNAME AppID\",\r\n  \"access.form.gname_app_id.tooltip\": \"For more information, see <a href=\\\"https://www.gname.com/user#/dealer_api\\\" target=\\\"_blank\\\">https://www.gname.com/user#/dealer_api</a>\",\r\n  \"access.form.gname_app_key.label\": \"GNAME AppKey\",\r\n  \"access.form.gname_app_key.placeholder\": \"Please enter GNAME AppKey\",\r\n  \"access.form.gname_app_key.tooltip\": \"For more information, see <a href=\\\"https://www.gname.com/user#/dealer_api\\\" target=\\\"_blank\\\">https://www.gname.com/user#/dealer_api</a>\",\r\n  \"access.form.godaddy_api_key.label\": \"GoDaddy API key\",\r\n  \"access.form.godaddy_api_key.placeholder\": \"Please enter GoDaddy API key\",\r\n  \"access.form.godaddy_api_key.tooltip\": \"For more information, see <a href=\\\"https://developer.godaddy.com/\\\" target=\\\"_blank\\\">https://developer.godaddy.com/</a>\",\r\n  \"access.form.godaddy_api_secret.label\": \"GoDaddy API secret\",\r\n  \"access.form.godaddy_api_secret.placeholder\": \"Please enter GoDaddy API secret\",\r\n  \"access.form.godaddy_api_secret.tooltip\": \"For more information, see <a href=\\\"https://developer.godaddy.com/\\\" target=\\\"_blank\\\">https://developer.godaddy.com/</a>\",\r\n  \"access.form.goedge_server_url.label\": \"GoEdge server URL\",\r\n  \"access.form.goedge_server_url.placeholder\": \"Please enter GoEdge server URL\",\r\n  \"access.form.goedge_api_role.label\": \"GoEdge user role\",\r\n  \"access.form.goedge_api_role.placeholder\": \"Please select GoEdge user role\",\r\n  \"access.form.goedge_api_role.option.user.label\": \"Platform user\",\r\n  \"access.form.goedge_api_role.option.admin.label\": \"Administrator user\",\r\n  \"access.form.goedge_access_key_id.label\": \"GoEdge AccessKeyID\",\r\n  \"access.form.goedge_access_key_id.placeholder\": \"Please enter GoEdge AccessKeyID\",\r\n  \"access.form.goedge_access_key_id.tooltip\": \"For more information, see <a href=\\\"https://goedge.cloud/docs/API/Auth.md\\\" target=\\\"_blank\\\">https://goedge.cloud/docs/API/Auth.md</a>\",\r\n  \"access.form.goedge_access_key.label\": \"GoEdge AccessKey\",\r\n  \"access.form.goedge_access_key.placeholder\": \"Please enter GoEdge AccessKey\",\r\n  \"access.form.goedge_access_key.tooltip\": \"For more information, see <a href=\\\"https://goedge.cloud/docs/API/Auth.md\\\" target=\\\"_blank\\\">https://goedge.cloud/docs/API/Auth.md</a>\",\r\n  \"access.form.globalsignatlas_eab.guide\": \"Learn more about using EAB key in GlobalSign Atlas: <br><a href=\\\"https://www.globalsign.com/en/acme-automated-certificate-management\\\" target=\\\"_blank\\\">https://www.globalsign.com/en/acme-automated-certificate-management</a>\",\r\n  \"access.form.googletrustservices_eab.guide\": \"Learn more about using EAB key in Google Trust Services: <br><a href=\\\"https://cloud.google.com/certificate-manager/docs/public-ca-tutorial\\\" target=\\\"_blank\\\">https://cloud.google.com/certificate-manager/docs/public-ca-tutorial</a>\",\r\n  \"access.form.hetzner_api_token.label\": \"Hetzner API token\",\r\n  \"access.form.hetzner_api_token.placeholder\": \"Please enter Hetzner API token\",\r\n  \"access.form.hetzner_api_token.tooltip\": \"For more information, see <a href=\\\"https://docs.hetzner.com/cloud/api/getting-started/generating-api-token\\\" target=\\\"_blank\\\">https://docs.hetzner.com/cloud/api/getting-started/generating-api-token</a>\",\r\n  \"access.form.hostingde_api_key.label\": \"hosting.de API key\",\r\n  \"access.form.hostingde_api_key.placeholder\": \"Please enter hosting.de API key\",\r\n  \"access.form.hostingde_api_key.tooltip\": \"For more information, see <a href=\\\"https://www.hosting.de/api/#requests-and-authentication\\\" target=\\\"_blank\\\">https://www.hosting.de/api/#requests-and-authentication</a>\",\r\n  \"access.form.hostinger_api_token.label\": \"Hostinger API token\",\r\n  \"access.form.hostinger_api_token.placeholder\": \"Please enter Hostinger API token\",\r\n  \"access.form.hostinger_api_token.tooltip\": \"For more information, see <a href=\\\"https://developers.hostinger.com/#description/authentication\\\" target=\\\"_blank\\\">https://developers.hostinger.com/#description/authentication</a>\",\r\n  \"access.form.huaweicloud_access_key_id.label\": \"Huawei Cloud AccessKeyID\",\r\n  \"access.form.huaweicloud_access_key_id.placeholder\": \"Please enter Huawei Cloud AccessKeyID\",\r\n  \"access.form.huaweicloud_access_key_id.tooltip\": \"For more information, see <a href=\\\"https://support.huaweicloud.com/intl/en-us/usermanual-ca/ca_01_0003.html\\\" target=\\\"_blank\\\">https://support.huaweicloud.com/intl/en-us/usermanual-ca/ca_01_0003.html</a>\",\r\n  \"access.form.huaweicloud_secret_access_key.label\": \"Huawei Cloud SecretAccessKey\",\r\n  \"access.form.huaweicloud_secret_access_key.placeholder\": \"Please enter Huawei Cloud SecretAccessKey\",\r\n  \"access.form.huaweicloud_secret_access_key.tooltip\": \"For more information, see <a href=\\\"https://support.huaweicloud.com/intl/en-us/usermanual-ca/ca_01_0003.html\\\" target=\\\"_blank\\\">https://support.huaweicloud.com/intl/en-us/usermanual-ca/ca_01_0003.html</a>\",\r\n  \"access.form.huaweicloud_enterprise_project_id.label\": \"Huawei Cloud enterprise project ID (Optional)\",\r\n  \"access.form.huaweicloud_enterprise_project_id.placeholder\": \"Please enter Huawei Cloud enterprise project ID\",\r\n  \"access.form.huaweicloud_enterprise_project_id.tooltip\": \"For more information, see <a href=\\\"https://support.huaweicloud.com/intl/en-us/usermanual-em/em_03_0000.html\\\" target=\\\"_blank\\\">https://support.huaweicloud.com/intl/en-us/usermanual-em/em_03_0000.html</a>\",\r\n  \"access.form.infomaniak_access_token.label\": \"Infomaniak access token\",\r\n  \"access.form.infomaniak_access_token.placeholder\": \"Please enter Infomaniak access token\",\r\n  \"access.form.infomaniak_access_token.tooltip\": \"For more information, see <a href=\\\"https://manager.infomaniak.com/v3/infomaniak-api\\\" target=\\\"_blank\\\">https://manager.infomaniak.com/v3/infomaniak-api</a>\",\r\n  \"access.form.ionos_api_key_public_prefix.label\": \"IONOS API key public prefix\",\r\n  \"access.form.ionos_api_key_public_prefix.placeholder\": \"Please enter IONOS API key public prefix\",\r\n  \"access.form.ionos_api_key_public_prefix.tooltip\": \"For more information, see <a href=\\\"https://developer.hosting.ionos.com/docs/getstarted\\\" target=\\\"_blank\\\">https://developer.hosting.ionos.com/docs/getstarted</a>\",\r\n  \"access.form.ionos_api_key_secret.label\": \"IONOS API key secret\",\r\n  \"access.form.ionos_api_key_secret.placeholder\": \"Please enter IONOS API key secret\",\r\n  \"access.form.ionos_api_key_secret.tooltip\": \"For more information, see <a href=\\\"https://developer.hosting.ionos.com/docs/getstarted\\\" target=\\\"_blank\\\">https://developer.hosting.ionos.com/docs/getstarted</a>\",\r\n  \"access.form.jdcloud_access_key_id.label\": \"JD Cloud AccessKeyID\",\r\n  \"access.form.jdcloud_access_key_id.placeholder\": \"Please enter JD Cloud AccessKeyID\",\r\n  \"access.form.jdcloud_access_key_id.tooltip\": \"For more information, see <a href=\\\"https://docs.jdcloud.com/en/account-management/accesskey-management\\\" target=\\\"_blank\\\">https://docs.jdcloud.com/en/account-management/accesskey-management</a>\",\r\n  \"access.form.jdcloud_access_key_secret.label\": \"JD Cloud AccessKeySecret\",\r\n  \"access.form.jdcloud_access_key_secret.placeholder\": \"Please enter JD Cloud AccessKeySecret\",\r\n  \"access.form.jdcloud_access_key_secret.tooltip\": \"For more information, see <a href=\\\"https://docs.jdcloud.com/en/account-management/accesskey-management\\\" target=\\\"_blank\\\">https://docs.jdcloud.com/en/account-management/accesskey-management</a>\",\r\n  \"access.form.k8s_kubeconfig.label\": \"KubeConfig (Optional)\",\r\n  \"access.form.k8s_kubeconfig.placeholder\": \"Please enter KubeConfig file\",\r\n  \"access.form.k8s_kubeconfig.help\": \"Notes: Leave it blank to use the Pod's ServiceAccount.\",\r\n  \"access.form.k8s_kubeconfig.tooltip\": \"For more information, see <a href=\\\"https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/\\\" target=\\\"_blank\\\">https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/</a>\",\r\n  \"access.form.kong_server_url.label\": \"Kong admin API server URL\",\r\n  \"access.form.kong_server_url.placeholder\": \"Please enter Kong admin API server URL\",\r\n  \"access.form.kong_api_token.label\": \"Kong admin API token (Optional)\",\r\n  \"access.form.kong_api_token.placeholder\": \"Please enter Kong admin API token\",\r\n  \"access.form.kong_api_token.tooltip\": \"For more information, see <a href=\\\"https://developer.konghq.com/admin-api/\\\" target=\\\"_blank\\\">https://developer.konghq.com/admin-api/</a>\",\r\n  \"access.form.ksyun_access_key_id.label\": \"Kingsoft Cloud AccessKeyID\",\r\n  \"access.form.ksyun_access_key_id.placeholder\": \"Please enter Kingsoft Cloud AccessKeyID\",\r\n  \"access.form.ksyun_access_key_id.tooltip\": \"For more information, see <a href=\\\"https://endocs.ksyun.com/documents/37659\\\" target=\\\"_blank\\\">https://endocs.ksyun.com/documents/37659</a>\",\r\n  \"access.form.ksyun_secret_access_key.label\": \"Kingsoft Cloud SecretAccessKey\",\r\n  \"access.form.ksyun_secret_access_key.placeholder\": \"Please enter Kingsoft Cloud SecretAccessKey\",\r\n  \"access.form.ksyun_secret_access_key.tooltip\": \"For more information, see <a href=\\\"https://endocs.ksyun.com/documents/37659\\\" target=\\\"_blank\\\">https://endocs.ksyun.com/documents/37659</a>\",\r\n  \"access.form.larkbot_webhook_url.label\": \"Lark bot Webhook URL\",\r\n  \"access.form.larkbot_webhook_url.placeholder\": \"Please enter Lark bot Webhook URL\",\r\n  \"access.form.larkbot_webhook_url.tooltip\": \"For more information, see <a href=\\\"https://open.larksuite.com/document/client-docs/bot-v3/add-custom-bot\\\" target=\\\"_blank\\\">https://open.larksuite.com/document/client-docs/bot-v3/add-custom-bot</a>\",\r\n  \"access.form.larkbot_secret.label\": \"Lark bot secret\",\r\n  \"access.form.larkbot_secret.placeholder\": \"Please enter Lark bot secret\",\r\n  \"access.form.larkbot_secret.tooltip\": \"For more information, see <a href=\\\"hhttps://open.larksuite.com/document/client-docs/bot-v3/add-custom-bot\\\" target=\\\"_blank\\\">https://open.larksuite.com/document/client-docs/bot-v3/add-custom-bot</a>\",\r\n  \"access.form.larkbot_custom_payload.label\": \"Lark bot message payload (Optional)\",\r\n  \"access.form.larkbot_custom_payload.placeholder\": \"Please enter custom Lark bot message payload\",\r\n  \"access.form.larkbot_custom_payload.checkbox\": \"Customized message payload\",\r\n  \"access.form.lecdn_server_url.label\": \"LeCDN server URL\",\r\n  \"access.form.lecdn_server_url.placeholder\": \"Please enter LeCDN server URL\",\r\n  \"access.form.lecdn_api_version.label\": \"LeCDN version\",\r\n  \"access.form.lecdn_api_version.placeholder\": \"Please select LeCDN version\",\r\n  \"access.form.lecdn_api_role.label\": \"LeCDN user role\",\r\n  \"access.form.lecdn_api_role.placeholder\": \"Please select LeCDN user role\",\r\n  \"access.form.lecdn_api_role.option.client.label\": \"Client\",\r\n  \"access.form.lecdn_api_role.option.master.label\": \"Master\",\r\n  \"access.form.lecdn_username.label\": \"LeCDN username\",\r\n  \"access.form.lecdn_username.placeholder\": \"Please enter LeCDN username\",\r\n  \"access.form.lecdn_password.label\": \"LeCDN password\",\r\n  \"access.form.lecdn_password.placeholder\": \"Please enter LeCDN password\",\r\n  \"access.form.linode_access_token.label\": \"Linode access token\",\r\n  \"access.form.linode_access_token.placeholder\": \"Please enter Linode access token\",\r\n  \"access.form.linode_access_token.tooltip\": \"For more information, see <a href=\\\"https://techdocs.akamai.com/linode-api/reference/get-started\\\" target=\\\"_blank\\\">https://techdocs.akamai.com/linode-api/reference/get-started</a>\",\r\n  \"access.form.litessl_eab.guide\": \"Learn more about using EAB key in LiteSSL: <br><a href=\\\"https://freessl.cn/automation/eab-manager\\\" target=\\\"_blank\\\">https://freessl.cn/automation/eab-manager</a>\",\r\n  \"access.form.mattermost_server_url.label\": \"Mattermost server URL\",\r\n  \"access.form.mattermost_server_url.placeholder\": \"Please enter Mattermost server URL\",\r\n  \"access.form.mattermost_username.label\": \"Mattermost username\",\r\n  \"access.form.mattermost_username.placeholder\": \"Please enter Mattermost username\",\r\n  \"access.form.mattermost_password.label\": \"Mattermost password\",\r\n  \"access.form.mattermost_password.placeholder\": \"Please enter Mattermost password\",\r\n  \"access.form.mattermost_channel_id.label\": \"Mattermost channel ID (Optional)\",\r\n  \"access.form.mattermost_channel_id.placeholder\": \"Please enter the default Mattermost channel ID\",\r\n  \"access.form.mattermost_channel_id.help\": \"Notes: It can be overrided in the workflows.\",\r\n  \"access.form.mattermost_channel_id.tooltip\": \"How to get it? Select the target channel from the left sidebar, click on the channel name at the top, and choose ”Channel Details.” You can directly see the channel ID on the pop-up page.\",\r\n  \"access.form.mohua_username.label\": \"Mohua Cloud username\",\r\n  \"access.form.mohua_username.placeholder\": \"Please enter MoHua Cloud username\",\r\n  \"access.form.mohua_api_password.label\": \"Mohua Cloud API password\",\r\n  \"access.form.mohua_api_password.placeholder\": \"Please enter Mohua Cloud API password\",\r\n  \"access.form.mohua_api_password.tooltip\": \"For more information, see <a href=\\\"https://cloud.mhjz1.cn/apimanage\\\" target=\\\"_blank\\\">https://cloud.mhjz1.cn/apimanage</a>\",\r\n  \"access.form.namecheap_username.label\": \"Namecheap username\",\r\n  \"access.form.namecheap_username.placeholder\": \"Please enter Namecheap username\",\r\n  \"access.form.namecheap_username.tooltip\": \"For more information, see <a href=\\\"https://www.namecheap.com/support/api/intro/\\\" target=\\\"_blank\\\">https://www.namecheap.com/support/api/intro/</a>\",\r\n  \"access.form.namecheap_api_key.label\": \"Namecheap API key\",\r\n  \"access.form.namecheap_api_key.placeholder\": \"Please enter Namecheap API key\",\r\n  \"access.form.namecheap_api_key.tooltip\": \"For more information, see <a href=\\\"https://www.namecheap.com/support/api/intro/\\\" target=\\\"_blank\\\">https://www.namecheap.com/support/api/intro/</a>\",\r\n  \"access.form.namedotcom_username.label\": \"Name.com username\",\r\n  \"access.form.namedotcom_username.placeholder\": \"Please enter Name.com username\",\r\n  \"access.form.namedotcom_username.tooltip\": \"For more information, see <a href=\\\"https://www.name.com/account/settings/api\\\" target=\\\"_blank\\\">https://www.name.com/account/settings/api</a>\",\r\n  \"access.form.namedotcom_api_token.label\": \"Name.com API token\",\r\n  \"access.form.namedotcom_api_token.placeholder\": \"Please enter Name.com API token\",\r\n  \"access.form.namedotcom_api_token.tooltip\": \"For more information, see <a href=\\\"https://www.name.com/support/articles/31142639244819-how-to-manage-your-api-tokens\\\" target=\\\"_blank\\\">https://www.name.com/support/articles/31142639244819-how-to-manage-your-api-tokens</a>\",\r\n  \"access.form.namesilo_api_key.label\": \"NameSilo API key\",\r\n  \"access.form.namesilo_api_key.placeholder\": \"Please enter NameSilo API key\",\r\n  \"access.form.namesilo_api_key.tooltip\": \"For more information, see <a href=\\\"https://www.namesilo.com/support/v2/articles/account-options/api-manager\\\" target=\\\"_blank\\\">https://www.namesilo.com/support/v2/articles/account-options/api-manager</a>\",\r\n  \"access.form.netlify_api_token.label\": \"Netlify API token\",\r\n  \"access.form.netlify_api_token.placeholder\": \"Please enter Netlify API token\",\r\n  \"access.form.netlify_api_token.tooltip\": \"For more information, see <a href=\\\"https://docs.netlify.com/api/get-started/#authentication\\\" target=\\\"_blank\\\">https://docs.netlify.com/api/get-started/#authentication</a>\",\r\n  \"access.form.netcup_customer_number.label\": \"netcup customer number\",\r\n  \"access.form.netcup_customer_number.placeholder\": \"Please enter netcup customer number\",\r\n  \"access.form.netcup_customer_number.tooltip\": \"For more information, see <a href=\\\"https://helpcenter.netcup.com/en/wiki/general/ccp-login/\\\" target=\\\"_blank\\\">https://helpcenter.netcup.com/en/wiki/general/ccp-login/</a>\",\r\n  \"access.form.netcup_api_key.label\": \"netcup API key\",\r\n  \"access.form.netcup_api_key.placeholder\": \"Please enter netcup API key\",\r\n  \"access.form.netcup_api_key.tooltip\": \"For more information, see <a href=\\\"https://helpcenter.netcup.com/en/wiki/general/our-api/\\\" target=\\\"_blank\\\">https://helpcenter.netcup.com/en/wiki/general/our-api/</a>\",\r\n  \"access.form.netcup_api_password.label\": \"netcup API password\",\r\n  \"access.form.netcup_api_password.placeholder\": \"Please enter netcup API password\",\r\n  \"access.form.netcup_api_password.tooltip\": \"For more information, see <a href=\\\"https://helpcenter.netcup.com/en/wiki/general/our-api/\\\" target=\\\"_blank\\\">https://helpcenter.netcup.com/en/wiki/general/our-api/</a>\",\r\n  \"access.form.nginxproxymanager_server_url.label\": \"NPM server URL\",\r\n  \"access.form.nginxproxymanager_server_url.placeholder\": \"Please enter NPM server URL\",\r\n  \"access.form.nginxproxymanager_auth_method.label\": \"NPM API authentication method\",\r\n  \"access.form.nginxproxymanager_auth_method.placeholder\": \"Please select NPM API authentication method\",\r\n  \"access.form.nginxproxymanager_auth_method.option.password.label\": \"Username & Password\",\r\n  \"access.form.nginxproxymanager_auth_method.option.token.label\": \"API token\",\r\n  \"access.form.nginxproxymanager_username.label\": \"NPM administrator username\",\r\n  \"access.form.nginxproxymanager_username.placeholder\": \"Please enter NPM administrator username\",\r\n  \"access.form.nginxproxymanager_password.label\": \"NPM administrator password\",\r\n  \"access.form.nginxproxymanager_password.placeholder\": \"Please enter NPM administrator password\",\r\n  \"access.form.nginxproxymanager_api_token.label\": \"NPM API token\",\r\n  \"access.form.nginxproxymanager_api_token.placeholder\": \"Please enter NPM API token\",\r\n  \"access.form.ns1_api_key.label\": \"NS1 API key\",\r\n  \"access.form.ns1_api_key.placeholder\": \"Please enter NS1 API key\",\r\n  \"access.form.ns1_api_key.tooltip\": \"For more information, see <a href=\\\"https://www.ibm.com/docs/en/ns1-connect?topic=introduction-using-api\\\" target=\\\"_blank\\\">https://www.ibm.com/docs/en/ns1-connect?topic=introduction-using-api</a>\",\r\n  \"access.form.ovhcloud_endpoint.label\": \"OVHcloud API endpoint\",\r\n  \"access.form.ovhcloud_endpoint.placeholder\": \"Please enter OVHcloud API endpoint\",\r\n  \"access.form.ovhcloud_auth_method.label\": \"OVHcloud API authentication method\",\r\n  \"access.form.ovhcloud_auth_method.placeholder\": \"Please select OVHcloud API authentication method\",\r\n  \"access.form.ovhcloud_auth_method.option.application.label\": \"Application key & Secret\",\r\n  \"access.form.ovhcloud_auth_method.option.oauth2.label\": \"OAuth2 client credentials\",\r\n  \"access.form.ovhcloud_application_key.label\": \"OVHcloud application key\",\r\n  \"access.form.ovhcloud_application_key.placeholder\": \"Please enter OVHcloud application key\",\r\n  \"access.form.ovhcloud_application_key.tooltip\": \"For more information, see <a href=\\\"https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/\\\" target=\\\"_blank\\\">https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/</a>\",\r\n  \"access.form.ovhcloud_application_secret.label\": \"OVHcloud application secret\",\r\n  \"access.form.ovhcloud_application_secret.placeholder\": \"Please enter OVHcloud application secret\",\r\n  \"access.form.ovhcloud_application_secret.tooltip\": \"For more information, see <a href=\\\"https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/\\\" target=\\\"_blank\\\">https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/</a>\",\r\n  \"access.form.ovhcloud_consumer_key.label\": \"OVHcloud consumer key\",\r\n  \"access.form.ovhcloud_consumer_key.placeholder\": \"Please enter OVHcloud consumer key\",\r\n  \"access.form.ovhcloud_consumer_key.tooltip\": \"For more information, see <a href=\\\"https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/\\\" target=\\\"_blank\\\">https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/</a>\",\r\n  \"access.form.ovhcloud_client_id.label\": \"OVHcloud client ID\",\r\n  \"access.form.ovhcloud_client_id.placeholder\": \"Please enter OVHcloud client ID\",\r\n  \"access.form.ovhcloud_client_id.tooltip\": \"For more information, see <a href=\\\"https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343\\\" target=\\\"_blank\\\">https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343</a>\",\r\n  \"access.form.ovhcloud_client_secret.label\": \"OVHcloud client secret\",\r\n  \"access.form.ovhcloud_client_secret.placeholder\": \"Please enter OVHcloud client secret\",\r\n  \"access.form.ovhcloud_client_secret.tooltip\": \"For more information, see <a href=\\\"https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343\\\" target=\\\"_blank\\\">https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343</a>\",\r\n  \"access.form.porkbun_api_key.label\": \"Porkbun API key\",\r\n  \"access.form.porkbun_api_key.placeholder\": \"Please enter Porkbun API key\",\r\n  \"access.form.porkbun_api_key.tooltip\": \"For more information, see <a href=\\\"https://porkbun.com/api/json/v3/documentation#Authentication\\\" target=\\\"_blank\\\">https://porkbun.com/api/json/v3/documentation</a>\",\r\n  \"access.form.porkbun_secret_api_key.label\": \"Porkbun secret API key\",\r\n  \"access.form.porkbun_secret_api_key.placeholder\": \"Please enter Porkbun secret API key\",\r\n  \"access.form.porkbun_secret_api_key.tooltip\": \"For more information, see <a href=\\\"https://porkbun.com/api/json/v3/documentation#Authentication\\\" target=\\\"_blank\\\">https://porkbun.com/api/json/v3/documentation</a>\",\r\n  \"access.form.powerdns_server_url.label\": \"PowerDNS server URL\",\r\n  \"access.form.powerdns_server_url.placeholder\": \"Please enter PowerDNS server URL\",\r\n  \"access.form.powerdns_api_key.label\": \"PowerDNS API key\",\r\n  \"access.form.powerdns_api_key.placeholder\": \"Please enter PowerDNS API key\",\r\n  \"access.form.powerdns_api_key.tooltip\": \"For more information, see <a href=\\\"https://doc.powerdns.com/authoritative/http-api/index.html#enabling-the-api\\\" target=\\\"_blank\\\">https://doc.powerdns.com/authoritative/http-api/index.html#enabling-the-api</a>\",\r\n  \"access.form.proxmoxve_server_url.label\": \"Proxmox VE server URL\",\r\n  \"access.form.proxmoxve_server_url.placeholder\": \"Please enter Proxmox VE server URL\",\r\n  \"access.form.proxmoxve_api_token.label\": \"Proxmox VE API token\",\r\n  \"access.form.proxmoxve_api_token.placeholder\": \"Please enter Proxmox VE API token\",\r\n  \"access.form.proxmoxve_api_token.tooltip\": \"For more information, see <a href=\\\"https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens\\\" target=\\\"_blank\\\">https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens</a>\",\r\n  \"access.form.proxmoxve_api_token_secret.label\": \"Proxmox VE API token secret (Optional)\",\r\n  \"access.form.proxmoxve_api_token_secret.placeholder\": \"Please enter Proxmox VE API token secret\",\r\n  \"access.form.proxmoxve_api_token_secret.tooltip\": \"For more information, see <a href=\\\"https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens\\\" target=\\\"_blank\\\">https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens</a>\",\r\n  \"access.form.qingcloud_access_key_id.label\": \"QingCloud AccessKeyID\",\r\n  \"access.form.qingcloud_access_key_id.placeholder\": \"Please enter QingCloud AccessKeyID\",\r\n  \"access.form.qingcloud_access_key_id.tooltip\": \"For more information, see <a href=\\\"https://console.qingcloud.com/access_keys/\\\" target=\\\"_blank\\\">https://console.qingcloud.com/access_keys/</a>\",\r\n  \"access.form.qingcloud_secret_access_key.label\": \"QingCloud SecretAccessKey\",\r\n  \"access.form.qingcloud_secret_access_key.placeholder\": \"Please enter QingCloud SecretAccessKey\",\r\n  \"access.form.qingcloud_secret_access_key.tooltip\": \"For more information, see <a href=\\\"https://console.qingcloud.com/access_keys/\\\" target=\\\"_blank\\\">https://console.qingcloud.com/access_keys/</a>\",\r\n  \"access.form.qiniu_access_key.label\": \"Qiniu AccessKey\",\r\n  \"access.form.qiniu_access_key.placeholder\": \"Please enter Qiniu AccessKey\",\r\n  \"access.form.qiniu_access_key.tooltip\": \"For more information, see <a href=\\\"https://portal.qiniu.com/\\\" target=\\\"_blank\\\">https://portal.qiniu.com/</a>\",\r\n  \"access.form.qiniu_secret_key.label\": \"Qiniu SecretKey\",\r\n  \"access.form.qiniu_secret_key.placeholder\": \"Please enter Qiniu SecretKey\",\r\n  \"access.form.qiniu_secret_key.tooltip\": \"For more information, see <a href=\\\"https://portal.qiniu.com/\\\" target=\\\"_blank\\\">https://portal.qiniu.com/</a>\",\r\n  \"access.form.rainyun_api_key.label\": \"Rain Yun API key\",\r\n  \"access.form.rainyun_api_key.placeholder\": \"Please enter Rain Yun API key\",\r\n  \"access.form.rainyun_api_key.tooltip\": \"For more information, see <a href=\\\"https://app.rainyun.com/account/settings/api-key\\\" target=\\\"_blank\\\">https://app.rainyun.com/account/settings/api-key</a>\",\r\n  \"access.form.ratpanel_server_url.label\": \"RatPanel server URL\",\r\n  \"access.form.ratpanel_server_url.placeholder\": \"Please enter RatPanel server URL\",\r\n  \"access.form.ratpanel_server_url.help\": \"Notes: DO NOT include the security entrance suffix.\",\r\n  \"access.form.ratpanel_access_token_id.label\": \"RatPanel access token ID\",\r\n  \"access.form.ratpanel_access_token_id.placeholder\": \"Please enter RatPanel access token ID\",\r\n  \"access.form.ratpanel_access_token_id.tooltip\": \"For more information, see <a href=\\\"https://ratpanel.github.io/advanced/api.html\\\" target=\\\"_blank\\\">https://ratpanel.github.io/advanced/api.html</a>\",\r\n  \"access.form.ratpanel_access_token.label\": \"RatPanel access token\",\r\n  \"access.form.ratpanel_access_token.placeholder\": \"Please enter RatPanel access token\",\r\n  \"access.form.ratpanel_access_token.tooltip\": \"For more information, see <a href=\\\"https://ratpanel.github.io/advanced/api.html\\\" target=\\\"_blank\\\">https://ratpanel.github.io/advanced/api.html</a>\",\r\n  \"access.form.rfc2136_host.label\": \"DNS server host\",\r\n  \"access.form.rfc2136_host.placeholder\": \"Please enter DNS server host\",\r\n  \"access.form.rfc2136_port.label\": \"DNS server port\",\r\n  \"access.form.rfc2136_port.placeholder\": \"Please enter DNS server port\",\r\n  \"access.form.rfc2136_tsig_algorithm.label\": \"TSIG algorithm\",\r\n  \"access.form.rfc2136_tsig_algorithm.placeholder\": \"Please select TSIG algorithm\",\r\n  \"access.form.rfc2136_tsig_key.label\": \"TSIG authentication key (Optional)\",\r\n  \"access.form.rfc2136_tsig_key.placeholder\": \"Please enter TSIG authentication key\",\r\n  \"access.form.rfc2136_tsig_secret.label\": \"TSIG authentication secret (Optional)\",\r\n  \"access.form.rfc2136_tsig_secret.placeholder\": \"Please enter TSIG authentication secret\",\r\n  \"access.form.s3_endpoint.label\": \"Endpoint\",\r\n  \"access.form.s3_endpoint.placeholder\": \"Please enter endpoint\",\r\n  \"access.form.s3_endpoint.help\": \"Notes: If the protocol is not specified, <em>https://</em> is used by default.\",\r\n  \"access.form.s3_access_key.label\": \"Access key\",\r\n  \"access.form.s3_access_key.placeholder\": \"Please enter access key\",\r\n  \"access.form.s3_secret_key.label\": \"Secret key\",\r\n  \"access.form.s3_secret_key.placeholder\": \"Please enter secret key\",\r\n  \"access.form.s3_signature_version.label\": \"Signature version\",\r\n  \"access.form.s3_signature_version.placeholder\": \"Please select signature version\",\r\n  \"access.form.s3_use_path_style.label\": \"Use path style addressing\",\r\n  \"access.form.s3_use_path_style.tooltip\": \"<ol style=\\\"list-style: disc;\\\"><li>Virtual-hosted style (default): https://&lt;BUCKET&gt;.&lt;ENDPOINT&gt;/&lt;KEY&gt; </li><li>Path style: https://&lt;ENDPOINT&gt;/&lt;BUCKET&gt;/&lt;KEY&gt; </li></ol>\",\r\n  \"access.form.safeline_server_url.label\": \"SafeLine server URL\",\r\n  \"access.form.safeline_server_url.placeholder\": \"Please enter SafeLine server URL\",\r\n  \"access.form.safeline_api_token.label\": \"SafeLine API token\",\r\n  \"access.form.safeline_api_token.placeholder\": \"Please enter SafeLine API token\",\r\n  \"access.form.safeline_api_token.tooltip\": \"For more information, see <a href=\\\"https://docs.waf.chaitin.com/en/reference/articles/openapi\\\" target=\\\"_blank\\\">https://docs.waf.chaitin.com/en/reference/articles/openapi</a>\",\r\n  \"access.form.sectigo_validation_type.label\": \"Domain validation type\",\r\n  \"access.form.sectigo_validation_type.placeholder\": \"Please select domain validation type\",\r\n  \"access.form.sectigo_validation_type.option.dv.label\": \"DV (Domain Validation)\",\r\n  \"access.form.sectigo_validation_type.option.ov.label\": \"OV (Organization Validation)\",\r\n  \"access.form.sectigo_validation_type.option.ev.label\": \"EV (Extended Validation)\",\r\n  \"access.form.sectigo_eab.guide\": \"Learn more about using EAB key in Sectigo: <br><a href=\\\"https://www.sectigo.com/enterprise-solutions/certificate-manager/integrations-acme\\\" target=\\\"_blank\\\">https://www.sectigo.com/enterprise-solutions/certificate-manager/integrations-acme</a>\",\r\n  \"access.form.slackbot_token.label\": \"Slack bot token\",\r\n  \"access.form.slackbot_token.placeholder\": \"Please enter Slack bot token\",\r\n  \"access.form.slackbot_token.tooltip\": \"For more information, see <a href=\\\"https://docs.slack.dev/authentication/tokens#bot\\\" target=\\\"_blank\\\">https://docs.slack.dev/authentication/tokens#bot</a>\",\r\n  \"access.form.slackbot_channel_id.label\": \"Slack channel ID (Optional)\",\r\n  \"access.form.slackbot_channel_id.placeholder\": \"Please enter the default Slack channel ID\",\r\n  \"access.form.slackbot_channel_id.help\": \"Notes: It can be overrided in the workflows.\",\r\n  \"access.form.slackbot_channel_id.tooltip\": \"How to get it? Please refer to <a href=\\\"https://www.youtube.com/watch?v=Uz5Yi5C2pwQ\\\" target=\\\"_blank\\\">https://www.youtube.com/watch?v=Uz5Yi5C2pwQ</a>\",\r\n  \"access.form.spaceship_api_key.label\": \"Spaceship API key\",\r\n  \"access.form.spaceship_api_key.placeholder\": \"Please enter Spaceship API key\",\r\n  \"access.form.spaceship_api_key.tooltip\": \"For more information, see <a href=\\\"https://www.spaceship.com/application/api-manager/\\\" target=\\\"_blank\\\">https://www.spaceship.com/application/api-manager/</a>\",\r\n  \"access.form.spaceship_api_secret.label\": \"Spaceship API secret\",\r\n  \"access.form.spaceship_api_secret.placeholder\": \"Please enter Spaceship API secret\",\r\n  \"access.form.spaceship_api_secret.tooltip\": \"For more information, see <a href=\\\"https://www.spaceship.com/application/api-manager/\\\" target=\\\"_blank\\\">https://www.spaceship.com/application/api-manager/</a>\",\r\n  \"access.form.ssh_host.label\": \"Server host\",\r\n  \"access.form.ssh_host.placeholder\": \"Please enter server host\",\r\n  \"access.form.ssh_port.label\": \"Server port\",\r\n  \"access.form.ssh_port.placeholder\": \"Please enter server port\",\r\n  \"access.form.ssh_auth_method.label\": \"Authentication method\",\r\n  \"access.form.ssh_auth_method.placeholder\": \"Please select authentication method\",\r\n  \"access.form.ssh_auth_method.option.none.label\": \"None\",\r\n  \"access.form.ssh_auth_method.option.password.label\": \"Password\",\r\n  \"access.form.ssh_auth_method.option.key.label\": \"SSH key\",\r\n  \"access.form.ssh_username.label\": \"Username\",\r\n  \"access.form.ssh_username.placeholder\": \"Please enter username\",\r\n  \"access.form.ssh_password.label\": \"Password\",\r\n  \"access.form.ssh_password.placeholder\": \"Please enter password\",\r\n  \"access.form.ssh_key.label\": \"SSH key\",\r\n  \"access.form.ssh_key.placeholder\": \"Please enter SSH key\",\r\n  \"access.form.ssh_key_passphrase.label\": \"SSH key passphrase (Optional)\",\r\n  \"access.form.ssh_key_passphrase.placeholder\": \"Please enter SSH key passphrase\",\r\n  \"access.form.ssh_jump_servers.label\": \"Jump servers (Optional)\",\r\n  \"access.form.ssh_jump_servers.errmsg.invalid\": \"Please configure valid jump servers\",\r\n  \"access.form.ssh_jump_servers.item.label\": \"Jump server\",\r\n  \"access.form.ssh_jump_servers.add.button\": \"Add jump server\",\r\n  \"access.form.sslcom_eab.guide\": \"Learn more about using EAB key in SSL.com: <br><a href=\\\"https://www.ssl.com/how-to/generate-acme-credentials-for-reseller-customers/#ftoc-heading-6\\\" target=\\\"_blank\\\">https://www.ssl.com/how-to/generate-acme-credentials-for-reseller-customers/</a>\",\r\n  \"access.form.synologydsm_server_url.label\": \"Synology DSM server URL\",\r\n  \"access.form.synologydsm_server_url.placeholder\": \"http://192.168.1.100:5000\",\r\n  \"access.form.synologydsm_server_url.tooltip\": \"Format: http(s)://IP-or-hostname:port. Default port is 5000 for HTTP, 5001 for HTTPS. Example: http://192.168.1.100:5000 or https://nas.example.com:5001\",\r\n  \"access.form.synologydsm_username.label\": \"Synology DSM username\",\r\n  \"access.form.synologydsm_username.placeholder\": \"Please enter Synology DSM username\",\r\n  \"access.form.synologydsm_password.label\": \"Synology DSM password\",\r\n  \"access.form.synologydsm_password.placeholder\": \"Please enter Synology DSM password\",\r\n  \"access.form.synologydsm_totp_secret.label\": \"Synology DSM 2FA TOTP secret key (Optional)\",\r\n  \"access.form.synologydsm_totp_secret.placeholder\": \"Please enter Synology DSM 2FA TOTP secret key\",\r\n  \"access.form.synologydsm_totp_secret.help\": \"Notes: Only required when you set up 2FA sign-in to Synology DSM.\",\r\n  \"access.form.synologydsm_totp_secret.tooltip\": \"How to get it? The secret key can be found by clicking \\\"Can't scan it\\\" when setting up 2FA, or by using a QRCode scanning tool.\",\r\n  \"access.form.telegrambot_token.label\": \"Telegram bot token\",\r\n  \"access.form.telegrambot_token.placeholder\": \"Please enter Telegram bot token\",\r\n  \"access.form.telegrambot_token.tooltip\": \"How to get it? Please refer to <a href=\\\"https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a\\\" target=\\\"_blank\\\">https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a</a>\",\r\n  \"access.form.technitiumdns_server_url.label\": \"Technitium DNS server URL\",\r\n  \"access.form.technitiumdns_server_url.placeholder\": \"Please enter Technitium DNS server URL\",\r\n  \"access.form.technitiumdns_api_token.label\": \"Technitium DNS API token\",\r\n  \"access.form.technitiumdns_api_token.placeholder\": \"Please enter Technitium DNS API token\",\r\n  \"access.form.technitiumdns_api_token.tooltip\": \"For more information, see <a href=\\\"https://github.com/TechnitiumSoftware/DnsServer/blob/master/APIDOCS.md\\\" target=\\\"_blank\\\">https://github.com/TechnitiumSoftware/DnsServer/blob/master/APIDOCS.md</a>\",\r\n  \"access.form.telegrambot_chat_id.label\": \"Telegram chat ID (Optional)\",\r\n  \"access.form.telegrambot_chat_id.placeholder\": \"Please enter the default Telegram chat ID\",\r\n  \"access.form.telegrambot_chat_id.help\": \"Notes: It can be overrided in the workflows.\",\r\n  \"access.form.telegrambot_chat_id.tooltip\": \"How to get it? Please refer to <a href=\\\"https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a\\\" target=\\\"_blank\\\">https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a</a>\",\r\n  \"access.form.tencentcloud_secret_id.label\": \"Tencent Cloud SecretID\",\r\n  \"access.form.tencentcloud_secret_id.placeholder\": \"Please enter Tencent Cloud SecretID\",\r\n  \"access.form.tencentcloud_secret_id.tooltip\": \"For more information, see <a href=\\\"https://www.tencentcloud.com/zh/document/product/598/34228\\\" target=\\\"_blank\\\">https://www.tencentcloud.com/zh/document/product/598/34228</a>\",\r\n  \"access.form.tencentcloud_secret_key.label\": \"Tencent Cloud SecretKey\",\r\n  \"access.form.tencentcloud_secret_key.placeholder\": \"Please enter Tencent Cloud SecretKey\",\r\n  \"access.form.tencentcloud_secret_key.tooltip\": \"For more information, see <a href=\\\"https://www.tencentcloud.com/zh/document/product/598/34228\\\" target=\\\"_blank\\\">https://www.tencentcloud.com/zh/document/product/598/34228</a>\",\r\n  \"access.form.todaynic_user_id.label\": \"TodayNIC.com agent user ID\",\r\n  \"access.form.todaynic_user_id.placeholder\": \"Please enter TodayNIC.com agent user ID\",\r\n  \"access.form.todaynic_api_key.label\": \"TodayNIC.com agent API key\",\r\n  \"access.form.todaynic_api_key.placeholder\": \"Please enter TodayNIC.com agent API key\",\r\n  \"access.form.todaynic_agent.guide\": \"TodayNIC.com API only supports calls from agents. Learn more about this: <br><a href=\\\"https://docs.apipost.net/docs/detail/49dcef10a876000?target_id=371b384\\\" target=\\\"_blank\\\">https://docs.apipost.net/docs/detail/49dcef10a876000?target_id=371b384</a>\",\r\n  \"access.form.ucloud_private_key.label\": \"UCloud API private key\",\r\n  \"access.form.ucloud_private_key.placeholder\": \"Please enter UCloud API private key\",\r\n  \"access.form.ucloud_private_key.tooltip\": \"For more information, see <a href=\\\"https://console.ucloud-global.com/uaccount/api_manage\\\" target=\\\"_blank\\\">https://console.ucloud-global.com/uaccount/api_manage</a>\",\r\n  \"access.form.ucloud_public_key.label\": \"UCloud API public key\",\r\n  \"access.form.ucloud_public_key.placeholder\": \"Please enter UCloud API public key\",\r\n  \"access.form.ucloud_public_key.tooltip\": \"For more information, see <a href=\\\"https://console.ucloud-global.com/uaccount/api_manage\\\" target=\\\"_blank\\\">https://console.ucloud-global.com/uaccount/api_manage</a>\",\r\n  \"access.form.ucloud_project_id.label\": \"UCloud project ID (Optional)\",\r\n  \"access.form.ucloud_project_id.placeholder\": \"Please enter UCloud project ID\",\r\n  \"access.form.ucloud_project_id.tooltip\": \"For more information, see <a href=\\\"https://console.ucloud-global.com/uaccount/iam/project_manage\\\" target=\\\"_blank\\\">https://console.ucloud-global.com/uaccount/iam/project_manage</a>\",\r\n  \"access.form.unicloud_username.label\": \"uniCloud username\",\r\n  \"access.form.unicloud_username.placeholder\": \"Please enter uniCloud username\",\r\n  \"access.form.unicloud_password.label\": \"uniCloud password\",\r\n  \"access.form.unicloud_password.placeholder\": \"Please enter uniCloud password\",\r\n  \"access.form.upyun_username.label\": \"UPYUN subaccount username\",\r\n  \"access.form.upyun_username.placeholder\": \"Please enter UPYUN subaccount username\",\r\n  \"access.form.upyun_username.tooltip\": \"For more information, see <a href=\\\"https://console.upyun.com/account/subaccount/\\\" target=\\\"_blank\\\">https://console.upyun.com/account/subaccount/</a>\",\r\n  \"access.form.upyun_password.label\": \"UPYUN subaccount password\",\r\n  \"access.form.upyun_password.placeholder\": \"Please enter UPYUN subaccount password\",\r\n  \"access.form.upyun_password.tooltip\": \"For more information, see <a href=\\\"https://console.upyun.com/account/subaccount/\\\" target=\\\"_blank\\\">https://console.upyun.com/account/subaccount/</a>\",\r\n  \"access.form.vercel_api_access_token.label\": \"Vercel API access token\",\r\n  \"access.form.vercel_api_access_token.placeholder\": \"Please enter Vercel API access token\",\r\n  \"access.form.vercel_api_access_token.tooltip\": \"For more information, see <a href=\\\"https://vercel.com/guides/how-do-i-use-a-vercel-api-access-token\\\" target=\\\"_blank\\\">https://vercel.com/guides/how-do-i-use-a-vercel-api-access-token</a>\",\r\n  \"access.form.vercel_team_id.label\": \"Vercel team ID (Optional)\",\r\n  \"access.form.vercel_team_id.placeholder\": \"Please enter Vercel team ID\",\r\n  \"access.form.vercel_team_id.tooltip\": \"For more information, see <a href=\\\"https://vercel.com/docs/accounts#find-your-team-id\\\" target=\\\"_blank\\\">https://vercel.com/docs/accounts#find-your-team-id</a>\",\r\n  \"access.form.volcengine_access_key_id.label\": \"VolcEngine AccessKeyID\",\r\n  \"access.form.volcengine_access_key_id.placeholder\": \"Please enter VolcEngine AccessKeyID\",\r\n  \"access.form.volcengine_access_key_id.tooltip\": \"For more information, see <a href=\\\"https://www.volcengine.com/docs/6291/216571\\\" target=\\\"_blank\\\">https://www.volcengine.com/docs/6291/216571</a>\",\r\n  \"access.form.volcengine_secret_access_key.label\": \"VolcEngine SecretAccessKey\",\r\n  \"access.form.volcengine_secret_access_key.placeholder\": \"Please enter VolcEngine SecretAccessKey\",\r\n  \"access.form.volcengine_secret_access_key.tooltip\": \"For more information, see <a href=\\\"https://www.volcengine.com/docs/6291/216571\\\" target=\\\"_blank\\\">https://www.volcengine.com/docs/6291/216571</a>\",\r\n  \"access.form.vultr_api_key.label\": \"Vultr API key\",\r\n  \"access.form.vultr_api_key.placeholder\": \"Please enter Vultr API key\",\r\n  \"access.form.vultr_api_key.tooltip\": \"For more information, see <a href=\\\"https://docs.vultr.com/platform/other/users/manage-users/api-access/regenerate-user-api-key\\\" target=\\\"_blank\\\">https://docs.vultr.com/platform/other/users/manage-users/api-access/regenerate-user-api-key</a>\",\r\n  \"access.form.wangsu_access_key_id.label\": \"Wangsu Cloud AccessKeyID\",\r\n  \"access.form.wangsu_access_key_id.placeholder\": \"Please enter Wangsu Cloud AccessKeyID\",\r\n  \"access.form.wangsu_access_key_id.tooltip\": \"For more information, see <a href=\\\"https://en.wangsu.com/document/account-manage/15775\\\" target=\\\"_blank\\\">https://en.wangsu.com/document/account-manage/15775</a>\",\r\n  \"access.form.wangsu_access_key_secret.label\": \"Wangsu Cloud AccessKeySecret\",\r\n  \"access.form.wangsu_access_key_secret.placeholder\": \"Please enter Wangsu Cloud AccessKeySecret\",\r\n  \"access.form.wangsu_access_key_secret.tooltip\": \"For more information, see <a href=\\\"https://en.wangsu.com/document/account-manage/15775\\\" target=\\\"_blank\\\">https://en.wangsu.com/document/account-manage/15775</a>\",\r\n  \"access.form.wangsu_api_key.label\": \"Wangsu Cloud API key\",\r\n  \"access.form.wangsu_api_key.placeholder\": \"Please enter Wangsu Cloud API key\",\r\n  \"access.form.wangsu_api_key.tooltip\": \"For more information, see <a href=\\\"https://en.wangsu.com/document/account-manage/15776\\\" target=\\\"_blank\\\">https://en.wangsu.com/document/account-manage/15776</a>\",\r\n  \"access.form.webhook_url.label\": \"Webhook URL\",\r\n  \"access.form.webhook_url.placeholder\": \"Please enter Webhook URL\",\r\n  \"access.form.webhook_method.label\": \"Webhook request method\",\r\n  \"access.form.webhook_method.placeholder\": \"Please select Webhook request method\",\r\n  \"access.form.webhook_headers.label\": \"Webhook request headers (Optional)\",\r\n  \"access.form.webhook_headers.placeholder\": \"Please enter Webhook request headers\",\r\n  \"access.form.webhook_headers.errmsg.invalid\": \"Please enter a valid request headers\",\r\n  \"access.form.webhook_headers.tooltip\": \"Example: <br><i>Content-Type: application/json<br>User-Agent: certimate</i>\",\r\n  \"access.form.webhook_data.label\": \"Webhook data (Optional)\",\r\n  \"access.form.webhook_data.placeholder\": \"Please enter the default Webhook data\",\r\n  \"access.form.webhook_data.help\": \"Notes: It can be overrided in the workflows.\",\r\n  \"access.form.webhook_data.guide_for_deployment\": \"The Webhook data should be in JSON format. <br><br>The values in JSON support template variables, which will be replaced by actual values when sent to the Webhook URL. Supported variables: <br><ol style=\\\"list-style: disc;\\\"><li><strong>${CERTIMATE_DEPLOYER_COMMONNAME}</strong>: The primary domain or IP address of the certificate.</li><li><strong>${CERTIMATE_DEPLOYER_SUBJECTALTNAMES}</strong>: The domains or IP addresses of the certificate, separated by semicolons.</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE}</strong>: The PEM format content of the certificate file.</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE_SERVER}</strong>: The PEM format content of the server certificate file.</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE_INTERMEDIA}</strong>: The PEM format content of the intermediate CA certificate file.</li><li><strong>${CERTIMATE_DEPLOYER_PRIVATEKEY}</strong>: The PEM format content of the private key file.</li></ol><br>When the request method is GET, the data will be passed as query string. Otherwise, the data will be encoded in the format indicated by the Content-Type in the request headers. Supported formats: <br><ol style=\\\"list-style: disc;\\\"><li>application/json (default).</li><li>application/x-www-form-urlencoded: Nested data is not supported.</li><li>multipart/form-data: Nested data is not supported.</li>\",\r\n  \"access.form.webhook_data.guide_for_notification\": \"The Webhook data should be in JSON format. <br><br>The values in JSON support template variables, which will be replaced by actual values when sent to the Webhook URL. Supported variables: <br><ol style=\\\"list-style: disc;\\\"><li><strong>${CERTIMATE_NOTIFIER_SUBJECT}</strong>: The subject of notification.</li><li><strong>${CERTIMATE_NOTIFIER_MESSAGE}</strong>: The message of notification.</li></ol><br>When the request method is GET, the data will be passed as query string. Otherwise, the data will be encoded in the format indicated by the Content-Type in the request headers. Supported formats: <br><ol style=\\\"list-style: disc;\\\"><li>application/json (default).</li><li>application/x-www-form-urlencoded: Nested data is not supported.</li><li>multipart/form-data: Nested data is not supported.</li>\",\r\n  \"access.form.webhook_preset_data\": \"Use preset Webhook\",\r\n  \"access.form.webhook_preset_data.bark\": \"Bark\",\r\n  \"access.form.webhook_preset_data.gotify\": \"Gotify\",\r\n  \"access.form.webhook_preset_data.messagenest\": \"Message Nest\",\r\n  \"access.form.webhook_preset_data.ntfy\": \"ntfy\",\r\n  \"access.form.webhook_preset_data.pushme\": \"PushMe\",\r\n  \"access.form.webhook_preset_data.pushover\": \"Pushover\",\r\n  \"access.form.webhook_preset_data.pushplus\": \"PushPlus\",\r\n  \"access.form.webhook_preset_data.serverchan3\": \"ServerChan<sup>3</sup>\",\r\n  \"access.form.webhook_preset_data.serverchanturbo\": \"ServerChan<sup>Turbo</sup>\",\r\n  \"access.form.webhook_preset_data.wxpush\": \"WXPush\",\r\n  \"access.form.webhook_preset_data.common\": \"General data\",\r\n  \"access.form.wecombot_webhook_url.label\": \"WeCom bot Webhook URL\",\r\n  \"access.form.wecombot_webhook_url.placeholder\": \"Please enter WeCom bot Webhook URL\",\r\n  \"access.form.wecombot_webhook_url.tooltip\": \"For more information, see <a href=\\\"https://www.west.cn/CustomerCenter/doc/apiv2.html#12u3001u8eabu4efdu9a8cu8bc10a3ca20id3d12u3001u8eabu4efdu9a8cu8bc13e203ca3e\\\" target=\\\"_blank\\\">https://www.west.cn/CustomerCenter/doc/apiv2.html</a>\",\r\n  \"access.form.wecombot_custom_payload.label\": \"WeCom bot message payload (Optional)\",\r\n  \"access.form.wecombot_custom_payload.placeholder\": \"Please enter custom WeCom bot message payload\",\r\n  \"access.form.wecombot_custom_payload.checkbox\": \"Customized message payload\",\r\n  \"access.form.westcn_username.label\": \"West.cn agent username\",\r\n  \"access.form.westcn_username.placeholder\": \"Please enter West.cn agent username\",\r\n  \"access.form.westcn_api_password.label\": \"West.cn agent API password\",\r\n  \"access.form.westcn_api_password.placeholder\": \"Please enter West.cn agent API password\",\r\n  \"access.form.westcn_agent.guide\": \"West.cn API only supports calls from agents. Learn more about this: <br><a href=\\\"https://www.west.cn/CustomerCenter/doc/apiv2.html#12u3001u8eabu4efdu9a8cu8bc10a3ca20id3d12u3001u8eabu4efdu9a8cu8bc13e203ca3e\\\" target=\\\"_blank\\\">https://www.west.cn/CustomerCenter/doc/apiv2.html</a>\",\r\n  \"access.form.xinnet_agent_id.label\": \"Xinnet.com agent ID\",\r\n  \"access.form.xinnet_agent_id.placeholder\": \"Please enter Xinnet.com agent ID\",\r\n  \"access.form.xinnet_api_password.label\": \"Xinnet.com agent API password\",\r\n  \"access.form.xinnet_api_password.placeholder\": \"Please enter Xinnet.com agent API password\",\r\n  \"access.form.xinnet_agent.guide\": \"Xinnet.com API only supports calls from agents. Learn more about this: <br><a href=\\\"https://apidoc.xin.cn/doc-7283837\\\" target=\\\"_blank\\\">https://apidoc.xin.cn/doc-7283837</a>\",\r\n  \"access.form.zerossl_eab.guide\": \"Learn more about using EAB key in ZeroSSL: <br><a href=\\\"https://zerossl.com/documentation/acme/\\\" target=\\\"_blank\\\">https://zerossl.com/documentation/acme/</a>\"\r\n}\r\n"
  },
  {
    "path": "ui/src/i18n/locales/en/nls.certificate.json",
    "content": "{\n  \"certificate.page.title\": \"Certificates\",\n  \"certificate.page.subtitle\": \"SSL certificates contain the website's public key and the website's identity, along with related information. They are generated from the execution output of workflows.\",\n\n  \"certificate.nodata.title\": \"No Certificates\",\n  \"certificate.nodata.description\": \"It looks like you don't have any certificates. Get started by running a workflow.\",\n  \"certificate.nodata.button\": \"Go to workflows\",\n\n  \"certificate.search.placeholder\": \"Search by certificate name or serial number ...\",\n\n  \"certificate.action.view.menu\": \"View details\",\n  \"certificate.action.revoke.menu\": \"Revoke\",\n  \"certificate.action.revoke.modal.title\": \"Revoke \\\"{{name}}\\\"\",\n  \"certificate.action.revoke.modal.content\": \"Are you sure want to revoke this certificate? <br>This action cannot be undone.\",\n  \"certificate.action.delete.menu\": \"Delete\",\n  \"certificate.action.delete.modal.title\": \"Delete \\\"{{name}}\\\"\",\n  \"certificate.action.delete.modal.content\": \"Are you sure want to delete this certificate? <br>This action cannot be undone.\",\n  \"certificate.action.batch_delete.modal.title\": \"Delete certificates\",\n  \"certificate.action.batch_delete.modal.content\": \"Are you sure want to delete these {{count}} selected certificates? <br>This action cannot be undone.\",\n\n  \"certificate.props.subject_alt_names\": \"Name\",\n  \"certificate.props.validity\": \"Expiry\",\n  \"certificate.props.validity.left_days\": \"{{left}} / {{total}} days left\",\n  \"certificate.props.validity.less_than_a_day\": \"≤1 / {{total}} days left\",\n  \"certificate.props.validity.expired\": \"Expired\",\n  \"certificate.props.validity.expiration\": \"Expire on {{date}}\",\n  \"certificate.props.validity.filter.all\": \"All\",\n  \"certificate.props.validity.filter.expiring_soon\": \"Expiring soon\",\n  \"certificate.props.validity.filter.expired\": \"Expired\",\n  \"certificate.props.brand\": \"Brand\",\n  \"certificate.props.source\": \"Source\",\n  \"certificate.props.source.request\": \"Request\",\n  \"certificate.props.source.upload\": \"Upload\",\n  \"certificate.props.revoked\": \"Revoked\",\n  \"certificate.props.certificate\": \"Certificate chain\",\n  \"certificate.props.private_key\": \"Private key\",\n  \"certificate.props.serial_number\": \"Serial number\",\n  \"certificate.props.key_algorithm\": \"Key algorithm\",\n  \"certificate.props.issuer\": \"Issuer\",\n  \"certificate.props.created_at\": \"Created at\",\n  \"certificate.props.updated_at\": \"Updated at\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/en/nls.common.json",
    "content": "﻿{\n  \"common.button.add\": \"Add\",\n  \"common.button.cancel\": \"Cancel\",\n  \"common.button.close\": \"Close\",\n  \"common.button.confirm\": \"Confirm\",\n  \"common.button.copy\": \"Copy\",\n  \"common.button.create\": \"Create\",\n  \"common.button.delete\": \"Delete\",\n  \"common.button.download\": \"Download\",\n  \"common.button.edit\": \"Edit\",\n  \"common.button.more\": \"More\",\n  \"common.button.ok\": \"Ok\",\n  \"common.button.reload\": \"Reload\",\n  \"common.button.reset\": \"Reset\",\n  \"common.button.save\": \"Save changes\",\n  \"common.button.save_and_continue\": \"Save and continue\",\n  \"common.button.submit\": \"Submit\",\n  \"common.button.view\": \"View\",\n\n  \"common.text.copied\": \"Copied\",\n  \"common.text.import_from_file\": \"Import from file ...\",\n  \"common.text.happy_browser\": \"The browser version is too low to make Certimate WebUI working well. Recommend using modern browsers such as Google Chrome v119.0 or higher.\",\n  \"common.text.nodata\": \"No data available\",\n  \"common.text.nodata_failed\": \"Failed to load data\",\n  \"common.text.operation_confirm\": \"Operation confirm\",\n  \"common.text.operation_succeeded\": \"Operation succeeded\",\n  \"common.text.operation_failed\": \"Operation failed\",\n  \"common.text.request_error\": \"Request error\",\n  \"common.text.saved\": \"Saved successfully\",\n  \"common.text.saving\": \"Saving ...\",\n  \"common.text.search\": \"Search ...\",\n\n  \"common.menu.document\": \"Document\",\n  \"common.menu.theme\": \"Change theme\",\n  \"common.menu.locale\": \"Change language\",\n  \"common.menu.gethelp\": \"Get help\",\n  \"common.menu.logout\": \"Log-out\",\n\n  \"common.theme.light\": \"Light\",\n  \"common.theme.dark\": \"Dark\",\n  \"common.theme.system\": \"Auto\",\n\n  \"common.errmsg.string_max\": \"Please enter no more than {{max}} characters\",\n  \"common.errmsg.email_invalid\": \"Please enter a valid email address\",\n  \"common.errmsg.domain_invalid\": \"Please enter a valid domain name\",\n  \"common.errmsg.host_invalid\": \"Please enter a valid domain name or IP address\",\n  \"common.errmsg.port_invalid\": \"Please enter a valid port\",\n  \"common.errmsg.ip_invalid\": \"Please enter a valid IP address\",\n  \"common.errmsg.url_invalid\": \"Please enter a valid URL\",\n  \"access.errmsg.json_invalid\": \"Please enter a valid JSON string\",\n  \"common.errmsg.form_invalid\": \"Please check the form content\",\n\n  \"common.notifier.bark\": \"Bark\",\n  \"common.notifier.dingtalk\": \"DingTalk\",\n  \"common.notifier.email\": \"Email\",\n  \"common.notifier.gotify\": \"Gotify\",\n  \"common.notifier.lark\": \"Lark\",\n  \"common.notifier.mattermost\": \"Mattermost\",\n  \"common.notifier.pushover\": \"Pushover\",\n  \"common.notifier.pushplus\": \"PushPlus\",\n  \"common.notifier.serverchan\": \"ServerChan\",\n  \"common.notifier.telegram\": \"Telegram\",\n  \"common.notifier.webhook\": \"Webhook\",\n  \"common.notifier.wecom\": \"WeCom\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/en/nls.dashboard.json",
    "content": "﻿{\n  \"dashboard.page.title\": \"Dashboard\",\n\n  \"dashboard.statistics.all_certificates\": \"All certificates\",\n  \"dashboard.statistics.expiring_soon_certificates\": \"Expiring soon certificates\",\n  \"dashboard.statistics.expired_certificates\": \"Expired certificates\",\n  \"dashboard.statistics.all_workflows\": \"All workflows\",\n  \"dashboard.statistics.enabled_workflows\": \"Active workflows\",\n\n  \"dashboard.shortcut\": \"Shortcuts\",\n  \"dashboard.shortcut.create_workflow\": \"Create new workflow\",\n  \"dashboard.shortcut.change_account\": \"Change username or password\",\n  \"dashboard.shortcut.configure_ca\": \"Configure certificate authorities\",\n  \"dashboard.shortcut.upgrade\": \"New version available!\",\n\n  \"dashboard.recent_workflow_runs\": \"Recent workflow runs\",\n  \"dashboard.recent_workflow_runs.nodata.description\": \"It looks like you don't have any runs. Get started by running a workflow.\",\n  \"dashboard.recent_workflow_runs.nodata.button\": \"Go to workflows\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/en/nls.login.json",
    "content": "﻿{\n  \"login.username.label\": \"Username\",\n  \"login.username.placeholder\": \"Username/Email\",\n  \"login.username.errmsg.invalid\": \"Please enter a valid email address\",\n  \"login.password.label\": \"Password\",\n  \"login.password.placeholder\": \"Password\",\n  \"login.password.errmsg.invalid\": \"Password should be at least 10 characters\",\n  \"login.submit\": \"Log-in\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/en/nls.preset.json",
    "content": "﻿{\n  \"preset.page.title\": \"Presets\",\n  \"preset.page.subtitle\": \"Presets are a set of reusable data snippets, for filling forms conveniently.\",\n\n  \"preset.action.create.button\": \"Create preset\",\n  \"preset.action.create.modal.title\": \"Create preset\",\n  \"preset.action.modify.menu\": \"Edit\",\n  \"preset.action.modify.modal.title\": \"Edit preset\",\n  \"preset.action.delete.menu\": \"Delete\",\n  \"preset.action.delete.modal.title\": \"Delete \\\"{{name}}\\\"\",\n  \"preset.action.delete.modal.content\": \"Are you sure want to delete this preset? <br>This action cannot be undone.\",\n\n  \"preset.props.name\": \"Name\",\n  \"preset.props.usage.notification\": \"Notification template\",\n  \"preset.props.usage.notification.tips\": \"You can use these preset notification subjects and messages in workflow notification nodes.\",\n  \"preset.props.usage.script\": \"Script template\",\n  \"preset.props.usage.script.tips\": \"You can use these preset script commands in workflow deployment nodes (for <i>Local host</i>, <i>Remote host</i>, etc.).\",\n\n  \"preset.warning.excceeded\": \"The maximum number of presets has been reached\",\n  \"preset.form.name.label\": \"Preset name\",\n  \"preset.form.name.placeholder\": \"Please enter preset name\",\n  \"preset.form.name.errmsg.duplicated\": \"The name already exists, please use a different one.\",\n  \"preset.form.notification_subject.label\": \"Notification subject\",\n  \"preset.form.notification_subject.placeholder\": \"Please enter notification subject\",\n  \"preset.form.notification_message.label\": \"Notification message\",\n  \"preset.form.notification_message.placeholder\": \"Please enter notification message\",\n  \"preset.form.script_command.label\": \"Script command\",\n  \"preset.form.script_command.placeholder\": \"Please enter script command\",\n\n  \"preset.dropdown.notification.button\": \"Use preset notification\",\n  \"preset.dropdown.script.button\": \"Use preset script\",\n  \"preset.dropdown.option_group.builtin\": \"Built-in templates\",\n  \"preset.dropdown.option_group.custom\": \"Customized templates\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/en/nls.provider.json",
    "content": "{\r\n  \"provider.1panel\": \"1Panel\",\r\n  \"provider.1panel_console\": \"1Panel - Console itself\",\r\n  \"provider.35cn\": \"35.cn\",\r\n  \"provider.51dnscom\": \"51DNS.com\",\r\n  \"provider.acmeca\": \"Custom ACME CA Endpoint\",\r\n  \"provider.acmedns\": \"ACME-DNS\",\r\n  \"provider.acmehttpreq\": \"Custom ACME Challenge Validation Endpoint Based on HTTP Request\",\r\n  \"provider.actalisssl\": \"Actalis SSL\",\r\n  \"provider.akamai\": \"Akamai\",\r\n  \"provider.akamai_cdn\": \"Akamai - CDN (Content Delivery Network)\",\r\n  \"provider.akamai_edgedns\": \"Akamai - EdgeDNS\",\r\n  \"provider.aliyun\": \"Alibaba Cloud\",\r\n  \"provider.aliyun_alb\": \"Alibaba Cloud - ALB (Application Load Balancer)\",\r\n  \"provider.aliyun_apigw\": \"Alibaba Cloud - API Gateway\",\r\n  \"provider.aliyun_cas_deploy\": \"Alibaba Cloud - Deploy via CAS (Certificate Management Service)\",\r\n  \"provider.aliyun_cas_upload\": \"Alibaba Cloud - Upload to CAS (Certificate Management Service)\",\r\n  \"provider.aliyun_cdn\": \"Alibaba Cloud - CDN (Content Delivery Network)\",\r\n  \"provider.aliyun_clb\": \"Alibaba Cloud - CLB (Classic Load Balancer)\",\r\n  \"provider.aliyun_dcdn\": \"Alibaba Cloud - DCDN (Dynamic Route for Content Delivery Network)\",\r\n  \"provider.aliyun_ddospro\": \"Alibaba Cloud - Anti-DDoS Proxy\",\r\n  \"provider.aliyun_dns\": \"Alibaba Cloud - DNS\",\r\n  \"provider.aliyun_esa\": \"Alibaba Cloud - ESA (Edge Security Acceleration)\",\r\n  \"provider.aliyun_esa_saas\": \"Alibaba Cloud - ESA SaaS Manager (Edge Security Acceleration SaaS)\",\r\n  \"provider.aliyun_fc\": \"Alibaba Cloud - FC (Function Compute)\",\r\n  \"provider.aliyun_ga\": \"Alibaba Cloud - GA (Global Accelerator)\",\r\n  \"provider.aliyun_live\": \"Alibaba Cloud - ApsaraVideo Live\",\r\n  \"provider.aliyun_nlb\": \"Alibaba Cloud - NLB (Network Load Balancer)\",\r\n  \"provider.aliyun_oss\": \"Alibaba Cloud - OSS (Object Storage Service)\",\r\n  \"provider.aliyun_vod\": \"Alibaba Cloud - ApsaraVideo VOD (Video on Demand)\",\r\n  \"provider.aliyun_waf\": \"Alibaba Cloud - WAF (Web Application Firewall)\",\r\n  \"provider.apisix\": \"Apache APISIX\",\r\n  \"provider.arvancloud\": \"ArvanCloud\",\r\n  \"provider.aws\": \"AWS\",\r\n  \"provider.aws_acm\": \"AWS - ACM (Amazon Certificate Manager)\",\r\n  \"provider.aws_cloudfront\": \"AWS - CloudFront\",\r\n  \"provider.aws_iam\": \"AWS - IAM (Identity and Access Management)\",\r\n  \"provider.aws_route53\": \"AWS - Route53\",\r\n  \"provider.azure\": \"Azure\",\r\n  \"provider.azure_dns\": \"Azure - DNS\",\r\n  \"provider.azure_keyvault\": \"Azure - KeyVault\",\r\n  \"provider.baiducloud\": \"Baidu Cloud\",\r\n  \"provider.baiducloud_appblb\": \"Baidu Cloud - AppBLB (Application Baidu Load Balancer)\",\r\n  \"provider.baiducloud_blb\": \"Baidu Cloud - BLB (Load Balancer)\",\r\n  \"provider.baiducloud_cdn\": \"Baidu Cloud - CDN (Content Delivery Network)\",\r\n  \"provider.baiducloud_cert_upload\": \"Baidu Cloud - Upload to SSL Certificate Service\",\r\n  \"provider.baiducloud_dns\": \"Baidu Cloud - DNS\",\r\n  \"provider.baishan\": \"Baishan\",\r\n  \"provider.baishan_cdn\": \"Baishan - CDN (Content Delivery Network)\",\r\n  \"provider.baotapanel\": \"aaPanel (aka BaotaPanel)\",\r\n  \"provider.baotapanel_common\": \"aaPanel\",\r\n  \"provider.baotapanel_console\": \"aaPanel - Console itself\",\r\n  \"provider.baotapanelgo\": \"aaPanel WinGo (aka BaotaPanel WinGo)\",\r\n  \"provider.baotapanelgo_common\": \"aaPanel WinGo\",\r\n  \"provider.baotapanelgo_console\": \"aaPanel WinGo - Console itself\",\r\n  \"provider.baotawaf\": \"aaWAF (aka BaotaWAF)\",\r\n  \"provider.baotawaf_common\": \"aaWAF\",\r\n  \"provider.baotawaf_console\": \"aaWAF - Console itself\",\r\n  \"provider.bookmyname\": \"BookMyName\",\r\n  \"provider.bunny\": \"Bunny\",\r\n  \"provider.bunny_cdn\": \"Bunny - CDN (Content Delivery Network)\",\r\n  \"provider.byteplus\": \"BytePlus\",\r\n  \"provider.byteplus_cdn\": \"BytePlus - CDN (Content Delivery Network)\",\r\n  \"provider.cachefly\": \"CacheFly\",\r\n  \"provider.cdnfly\": \"Cdnfly\",\r\n  \"provider.cloudflare\": \"Cloudflare\",\r\n  \"provider.cloudns\": \"ClouDNS\",\r\n  \"provider.cmcccloud\": \"China Mobile ECloud\",\r\n  \"provider.cmcccloud_dns\": \"China Mobile ECloud - DNS\",\r\n  \"provider.constellix\": \"Constellix\",\r\n  \"provider.cpanel\": \"cPanel\",\r\n  \"provider.ctcccloud\": \"China Telecom StateCloud\",\r\n  \"provider.ctcccloud_ao\": \"China Telecom StateCloud - AccessOne\",\r\n  \"provider.ctcccloud_cdn\": \"China Telecom StateCloud - CDN (Content Delivery Network)\",\r\n  \"provider.ctcccloud_cms_upload\": \"China Telecom StateCloud - Upload to Certificate Management Service\",\r\n  \"provider.ctcccloud_elb\": \"China Telecom StateCloud - ELB (Elastic Load Balancing)\",\r\n  \"provider.ctcccloud_faas\": \"China Telecom StateCloud - FaaS (Function as a Service)\",\r\n  \"provider.ctcccloud_icdn\": \"China Telecom StateCloud - ICDN (Integrated Content Delivery Network)\",\r\n  \"provider.ctcccloud_lvdn\": \"China Telecom StateCloud - LVDN (Live Video Delivery Network)\",\r\n  \"provider.ctcccloud_smartdns\": \"China Telecom StateCloud - Smart DNS\",\r\n  \"provider.cucccloud\": \"China Unicom Cloud\",\r\n  \"provider.desec\": \"deSEC\",\r\n  \"provider.digicert\": \"DigiCert\",\r\n  \"provider.digitalocean\": \"DigitalOcean\",\r\n  \"provider.dingtalkbot\": \"DingTalk Bot\",\r\n  \"provider.discordbot\": \"Discord Bot\",\r\n  \"provider.dnsexit\": \"DNSExit\",\r\n  \"provider.dnsla\": \"DNS.LA\",\r\n  \"provider.dnsmadeeasy\": \"DNS Made Easy\",\r\n  \"provider.dogecloud_cdn\": \"Doge Cloud - CDN (Content Delivery Network)\",\r\n  \"provider.dogecloud\": \"Doge Cloud\",\r\n  \"provider.dokploy\": \"Dokploy\",\r\n  \"provider.duckdns\": \"Duck DNS\",\r\n  \"provider.dynu\": \"Dynu\",\r\n  \"provider.dynv6\": \"dynv6\",\r\n  \"provider.email\": \"Email (SMTP)\",\r\n  \"provider.fastly\": \"Fastly\",\r\n  \"provider.flexcdn\": \"FlexCDN\",\r\n  \"provider.flyio\": \"Fly.io\",\r\n  \"provider.gandinet\": \"Gandi.net\",\r\n  \"provider.gcore\": \"G-Core\",\r\n  \"provider.gcore_cdn\": \"G-Core - CDN (Content Delivery Network)\",\r\n  \"provider.globalsignatlas\": \"GlobalSign Atlas\",\r\n  \"provider.gname\": \"GNAME\",\r\n  \"provider.godaddy\": \"GoDaddy\",\r\n  \"provider.goedge\": \"GoEdge\",\r\n  \"provider.googletrustservices\": \"Google Trust Services\",\r\n  \"provider.hetzner\": \"Hetzner\",\r\n  \"provider.hostingde\": \"hosting.de\",\r\n  \"provider.hostinger\": \"Hostinger\",\r\n  \"provider.huaweicloud\": \"Huawei Cloud\",\r\n  \"provider.huaweicloud_cdn\": \"Huawei Cloud - CDN (Content Delivery Network)\",\r\n  \"provider.huaweicloud_dns\": \"Huawei Cloud - DNS\",\r\n  \"provider.huaweicloud_elb\": \"Huawei Cloud - ELB (Elastic Load Balance)\",\r\n  \"provider.huaweicloud_obs\": \"Huawei Cloud - OBS (Object Storage Service)\",\r\n  \"provider.huaweicloud_scm_upload\": \"Huawei Cloud - Upload to SCM (SSL Certificate Manager)\",\r\n  \"provider.huaweicloud_waf\": \"Huawei Cloud - WAF (Web Application Firewall)\",\r\n  \"provider.infomaniak\": \"Infomaniak\",\r\n  \"provider.ionos\": \"IONOS\",\r\n  \"provider.jdcloud\": \"JD Cloud\",\r\n  \"provider.jdcloud_alb\": \"JD Cloud - ALB (Application Load Balancer)\",\r\n  \"provider.jdcloud_cdn\": \"JD Cloud - CDN (Content Delivery Network)\",\r\n  \"provider.jdcloud_dns\": \"JD Cloud - DNS\",\r\n  \"provider.jdcloud_live\": \"JD Cloud - Live Video\",\r\n  \"provider.jdcloud_vod\": \"JD Cloud - VOD (Video on Demand)\",\r\n  \"provider.kong\": \"Kong\",\r\n  \"provider.kubernetes\": \"Kubernetes\",\r\n  \"provider.kubernetes_secret\": \"Kubernetes - Secret\",\r\n  \"provider.ksyun\": \"Kingsoft Cloud\",\r\n  \"provider.ksyun_cdn\": \"Kingsoft Cloud - CDN (Content Delivery Network)\",\r\n  \"provider.larkbot\": \"Lark Bot\",\r\n  \"provider.lecdn\": \"LeCDN\",\r\n  \"provider.letsencrypt\": \"Let's Encrypt\",\r\n  \"provider.letsencryptstaging\": \"Let's Encrypt Staging Environment\",\r\n  \"provider.linode\": \"Linode\",\r\n  \"provider.litessl\": \"LiteSSL\",\r\n  \"provider.local\": \"Local host\",\r\n  \"provider.mattermost\": \"Mattermost\",\r\n  \"provider.mohua\": \"Mohua\",\r\n  \"provider.mohua_mvh\": \"Mohua - MVH (Virtual Host)\",\r\n  \"provider.namecheap\": \"Namecheap\",\r\n  \"provider.namedotcom\": \"Name.com\",\r\n  \"provider.namesilo\": \"NameSilo\",\r\n  \"provider.netcup\": \"netcup\",\r\n  \"provider.netlify\": \"Netlify\",\r\n  \"provider.nginxproxymanager\": \"Nginx Proxy Manager\",\r\n  \"provider.ns1\": \"NS1 (IBM NS1 Connect)\",\r\n  \"provider.ovhcloud\": \"OVHcloud\",\r\n  \"provider.porkbun\": \"Porkbun\",\r\n  \"provider.powerdns\": \"PowerDNS\",\r\n  \"provider.proxmoxve\": \"Proxmox VE\",\r\n  \"provider.qingcloud\": \"QingCloud\",\r\n  \"provider.qingcloud_dns\": \"QingCloud - DNS\",\r\n  \"provider.qiniu\": \"Qiniu\",\r\n  \"provider.qiniu_cdn\": \"Qiniu - CDN (Content Delivery Network)\",\r\n  \"provider.qiniu_kodo\": \"Qiniu - Kodo\",\r\n  \"provider.qiniu_pili\": \"Qiniu - Pili\",\r\n  \"provider.rainyun\": \"Rain Yun\",\r\n  \"provider.rainyun_rcdn\": \"Rain Yun - RCDN (Content Delivery Network)\",\r\n  \"provider.rainyun_sslcenter_upload\": \"Rain Yun - Upload to SSL Certificate Center\",\r\n  \"provider.ratpanel\": \"AcePanel (aka RatPanel)\",\r\n  \"provider.ratpanel_common\": \"AcePanel\",\r\n  \"provider.ratpanel_console\": \"AcePanel - Console itself\",\r\n  \"provider.rfc2136\": \"RFC 2136: Dynamic DNS Updates\",\r\n  \"provider.s3\": \"Object storage (S3-compatible)\",\r\n  \"provider.s3_upload\": \"Upload to S3-compatible object storage\",\r\n  \"provider.safeline\": \"SafeLine\",\r\n  \"provider.sectigo\": \"Sectigo\",\r\n  \"provider.slackbot\": \"Slack Bot\",\r\n  \"provider.spaceship\": \"Spaceship\",\r\n  \"provider.ssh\": \"Remote host (SSH)\",\r\n  \"provider.sslcom\": \"SSL.com\",\r\n  \"provider.synologydsm\": \"Synology DSM\",\r\n  \"provider.technitiumdns\": \"Technitium DNS\",\r\n  \"provider.telegrambot\": \"Telegram Bot\",\r\n  \"provider.tencentcloud\": \"Tencent Cloud\",\r\n  \"provider.tencentcloud_cdn\": \"Tencent Cloud - CDN (Content Delivery Network)\",\r\n  \"provider.tencentcloud_clb\": \"Tencent Cloud - CLB (Cloud Load Balancer)\",\r\n  \"provider.tencentcloud_cos\": \"Tencent Cloud - COS (Cloud Object Storage)\",\r\n  \"provider.tencentcloud_css\": \"Tencent Cloud - CSS (Cloud Streaming Service)\",\r\n  \"provider.tencentcloud_dns\": \"Tencent Cloud - DNS\",\r\n  \"provider.tencentcloud_ecdn\": \"Tencent Cloud - ECDN (Enterprise Content Delivery Network)\",\r\n  \"provider.tencentcloud_eo\": \"Tencent Cloud - EdgeOne\",\r\n  \"provider.tencentcloud_gaap\": \"Tencent Cloud - GAAP (Global Application Acceleration Platform)\",\r\n  \"provider.tencentcloud_scf\": \"Tencent Cloud - SCF (Serverless Cloud Function)\",\r\n  \"provider.tencentcloud_ssl_deploy\": \"Tencent Cloud - Deploy via SSL Certificate Service\",\r\n  \"provider.tencentcloud_ssl_update\": \"Tencent Cloud - Update via SSL Certificate Service\",\r\n  \"provider.tencentcloud_ssl_upload\": \"Tencent Cloud - Upload to SSL Certificate Service\",\r\n  \"provider.tencentcloud_vod\": \"Tencent Cloud - VOD (Video on Demand)\",\r\n  \"provider.tencentcloud_waf\": \"Tencent Cloud - WAF (Web Application Firewall)\",\r\n  \"provider.ucloud\": \"UCloud\",\r\n  \"provider.ucloud_ualb\": \"UCloud - UALB (Application Load Balancer)\",\r\n  \"provider.ucloud_ucdn\": \"UCloud - UCDN (Content Delivery Network)\",\r\n  \"provider.ucloud_uclb\": \"UCloud - UCLB (Classic Load Balancer)\",\r\n  \"provider.ucloud_udnr\": \"UCloud - UDNR (Domain Name Registrar)\",\r\n  \"provider.ucloud_uewaf\": \"UCloud - UEWAF (Enterprise Web Application Firewall)\",\r\n  \"provider.ucloud_upathx\": \"UCloud - UPathX (Global Dynamic Acceleration)\",\r\n  \"provider.ucloud_us3\": \"UCloud - US3 (Object-based Storage)\",\r\n  \"provider.todaynic\": \"TodayNIC.com\",\r\n  \"provider.unicloud\": \"uniCloud (DCloud)\",\r\n  \"provider.unicloud_webhost\": \"uniCloud - Web Host\",\r\n  \"provider.upyun\": \"UPYUN\",\r\n  \"provider.upyun_cdn\": \"UPYUN - CDN (Content Delivery Network)\",\r\n  \"provider.upyun_file\": \"UPYUN - USS (Storage Service)\",\r\n  \"provider.vercel\": \"Vercel\",\r\n  \"provider.volcengine\": \"Volcengine\",\r\n  \"provider.volcengine_alb\": \"Volcengine - ALB (Application Load Balancer)\",\r\n  \"provider.volcengine_cdn\": \"Volcengine - CDN (Content Delivery Network)\",\r\n  \"provider.volcengine_certcenter_upload\": \"Volcengine - Upload to Certificate Center\",\r\n  \"provider.volcengine_clb\": \"Volcengine - CLB (Cloud Load Balancer)\",\r\n  \"provider.volcengine_dcdn\": \"Volcengine - DCDN (Dynamic Content Delivery Network)\",\r\n  \"provider.volcengine_dns\": \"Volcengine - DNS\",\r\n  \"provider.volcengine_imagex\": \"Volcengine - ImageX\",\r\n  \"provider.volcengine_live\": \"Volcengine - Live\",\r\n  \"provider.volcengine_tos\": \"Volcengine - TOS (Tinder Object Storage)\",\r\n  \"provider.volcengine_vod\": \"Volcengine - VOD (Video on Demand)\",\r\n  \"provider.volcengine_waf\": \"Volcengine - WAF (Web Application Firewall)\",\r\n  \"provider.vultr\": \"Vultr\",\r\n  \"provider.wangsu\": \"Wangsu Cloud\",\r\n  \"provider.wangsu_cdn\": \"Wangsu Cloud - CDN (Content Delivery Network)\",\r\n  \"provider.wangsu_cdnpro\": \"Wangsu Cloud - CDN Pro (CDN 360)\",\r\n  \"provider.wangsu_certificate_upload\": \"Wangsu Cloud - Upload to Certificate Management\",\r\n  \"provider.webhook\": \"Webhook\",\r\n  \"provider.wecombot\": \"WeCom Bot\",\r\n  \"provider.westcn\": \"West.cn\",\r\n  \"provider.xinnet\": \"Xinnet.com\",\r\n  \"provider.zerossl\": \"ZeroSSL\",\r\n\r\n  \"provider.category.all\": \"All\",\r\n  \"provider.category.cdn\": \"CDN\",\r\n  \"provider.category.storage\": \"Storage\",\r\n  \"provider.category.loadbalance\": \"Loadbalance\",\r\n  \"provider.category.firewall\": \"Firewall\",\r\n  \"provider.category.av\": \"Audio/Video\",\r\n  \"provider.category.accelerator\": \"Accelerator\",\r\n  \"provider.category.apigw\": \"API Gateway\",\r\n  \"provider.category.serverless\": \"Serverless\",\r\n  \"provider.category.website\": \"Website\",\r\n  \"provider.category.ssl\": \"SSL\",\r\n  \"provider.category.other\": \"Other\",\r\n\r\n  \"provider.text.nodata\": \"No providers available\",\r\n  \"provider.text.default_ca\": \"(Default) Follow global settings\",\r\n  \"provider.text.default_ca_in_group\": \"Follow global settings\",\r\n  \"provider.text.default_group\": \"Default\",\r\n  \"provider.text.available_group\": \"Available (with added credentials)\",\r\n  \"provider.text.unavailable_group\": \"Unavailable (without added credentials)\",\r\n  \"provider.text.unavailable_divider\": \"The following providers are not available (without added credentials)\"\r\n}\r\n"
  },
  {
    "path": "ui/src/i18n/locales/en/nls.settings.json",
    "content": "﻿{\n  \"settings.page.title\": \"Settings\",\n\n  \"settings.account.tab\": \"Account\",\n  \"settings.account.username.title\": \"Username\",\n  \"settings.account.username.tips\": \"Email you can use to sign in to your account.\",\n  \"settings.account.username.button.label\": \"Change email\",\n  \"settings.account.username.form.email.label\": \"Email\",\n  \"settings.account.username.form.email.placeholder\": \"Please enter email\",\n  \"settings.account.password.title\": \"Password\",\n  \"settings.account.password.tips\": \"It's recommended to change your password regularly, or sooner if you suspect any issues.\",\n  \"settings.account.password.button.label\": \"Change password\",\n  \"settings.account.password.form.email.old_password.label\": \"Current password\",\n  \"settings.account.password.form.email.old_password.placeholder\": \"Please enter the current password\",\n  \"settings.account.password.form.email.new_password.label\": \"New password\",\n  \"settings.account.password.form.email.new_password.placeholder\": \"Please enter the new password\",\n  \"settings.account.password.form.email.confirm_password.label\": \"Confirm password\",\n  \"settings.account.password.form.email.confirm_password.placeholder\": \"Please enter the new password again\",\n  \"settings.account.password.form.email.password.errmsg.invalid\": \"Password should be at least 10 characters\",\n  \"settings.account.password.form.email.password.errmsg.not_matched\": \"Passwords do not match\",\n  \"settings.account.2fa.title\": \"Two-factor authentication\",\n\n  \"settings.appearance.tab\": \"Appearance\",\n  \"settings.appearance.theme.title\": \"Theme\",\n  \"settings.appearance.theme.form.value.extra\": \"Reload the page for the change to take effect.\",\n  \"settings.appearance.language.title\": \"Language\",\n  \"settings.appearance.language.form.value.extra\": \"Reload the page for the change to take effect.\",\n  \"settings.appearance.pagination.title\": \"Pagination\",\n  \"settings.appearance.pagination.form.default_per_page.label\": \"Default items per page\",\n  \"settings.appearance.pagination.form.default_per_page.placeholder\": \"Please enter default items per page\",\n  \"settings.appearance.pagination.form.default_per_page.unit\": \"items per page\",\n  \"settings.appearance.workflow.title\": \"Workflow\",\n  \"settings.appearance.workflow.form.default_designer_layout.label\": \"Default designer layout\",\n  \"settings.appearance.workflow.form.default_designer_layout.placeholder\": \"Please select default designer layout\",\n  \"settings.appearance.workflow.form.default_designer_layout.option.horizontal\": \"Horizontal layout\",\n  \"settings.appearance.workflow.form.default_designer_layout.option.vertical\": \"Vertical layout\",\n\n  \"settings.sslprovider.tab\": \"Certificate authority\",\n  \"settings.sslprovider.ca.title\": \"System-wide CA\",\n  \"settings.sslprovider.ca.tips\": \"You can use different CAs for each workflow as well. If you want to do this, please visit the credentials page.\",\n  \"settings.sslprovider.ca.form.provider.label\": \"ACME CA provider\",\n  \"settings.sslprovider.ca.form.provider.help\": \"Notes: The certificate validity lifetime, certificate algorithm, domain names count, and support for wildcard domain names are allowed may vary among different providers. After switching service providers, please check whether the configuration of the workflows needs to be adjusted.\",\n  \"settings.sslprovider.ca.form.letsencryptstaging_alert\": \"The staging environment can reduce the chance of your running up against rate limits. <br><br>Learn more:<br><a href=\\\"https://letsencrypt.org/docs/staging-environment/\\\" target=\\\"_blank\\\">https://letsencrypt.org/docs/staging-environment/</a>\",\n  \"settings.sslprovider.others.title\": \"Others\",\n  \"settings.sslprovider.others.form.timeout.label\": \"Certificate obtaining timeout\",\n  \"settings.sslprovider.others.form.timeout.placeholder\": \"Please enter certificate obtaining timeout\",\n  \"settings.sslprovider.others.form.timeout.unit\": \"seconds\",\n  \"settings.sslprovider.others.form.timeout.tooltip\": \"If you don't understand this option, just keep it by default.\",\n\n  \"settings.persistence.tab\": \"Persistence\",\n  \"settings.persistence.alerting.title\": \"Alerting\",\n  \"settings.persistence.alerting.form.certificates_warning_days_before_expire.label\": \"Certificate expiration warning threshold\",\n  \"settings.persistence.alerting.form.certificates_warning_days_before_expire.placeholder\": \"Please enter the certificate expiration warning threshold\",\n  \"settings.persistence.alerting.form.certificates_warning_days_before_expire.unit\": \"days\",\n  \"settings.persistence.alerting.form.certificates_warning_days_before_expire.help\": \"Notes: It determines when the certificate will be marked as \\\"Expiring-Soon\\\".\",\n  \"settings.persistence.data_retention.title\": \"Data retention\",\n  \"settings.persistence.data_retention.form.workflow_runs_retention_max_days.label\": \"Workflow runs retention max days\",\n  \"settings.persistence.data_retention.form.workflow_runs_retention_max_days.placeholder\": \"Please enter the maximum number of retention days for workflow history runs\",\n  \"settings.persistence.data_retention.form.workflow_runs_retention_max_days.unit\": \"days\",\n  \"settings.persistence.data_retention.form.workflow_runs_retention_max_days.help\": \"Notes: Set to <b>0</b> to disable cleanup workflow history runs. Recommend setting to <b>180</b> days or more.\",\n  \"settings.persistence.data_retention.form.certificates_retention_max_days.label\": \"Expired certificates retention max days\",\n  \"settings.persistence.data_retention.form.certificates_retention_max_days.placeholder\": \"Please enter the maximum number of retention days for expired certificates\",\n  \"settings.persistence.data_retention.form.certificates_retention_max_days.unit\": \"days\",\n  \"settings.persistence.data_retention.form.certificates_retention_max_days.help\": \"Notes: Set to <b>0</emb> to disable cleanup expired certificates.\",\n\n  \"settings.diagnostics.tab\": \"Diagnostics\",\n  \"settings.diagnostics.logs.title\": \"System logs\",\n  \"settings.diagnostics.logs.button.refresh.label\": \"Refresh\",\n  \"settings.diagnostics.logs.button.load_more.label\": \"Load more\",\n  \"settings.diagnostics.crons.title\": \"CRON jobs\",\n  \"settings.diagnostics.crons.props.next_trigger_time\": \"Expected next execution time: \",\n  \"settings.diagnostics.workflow_dispatcher.title\": \"Workflow dispatcher\",\n  \"settings.diagnostics.workflow_dispatcher.statistics.concurrency\": \"Concurrency\",\n  \"settings.diagnostics.workflow_dispatcher.statistics.pending\": \"Pending\",\n  \"settings.diagnostics.workflow_dispatcher.statistics.processing\": \"Processing\",\n\n  \"settings.about.tab\": \"About\",\n  \"settings.about.version.new\": \"New version available\",\n  \"settings.about.socials.document\": \"Documentation\",\n  \"settings.about.socials.github\": \"GitHub\",\n  \"settings.about.socials.telegram\": \"Telegram\",\n  \"settings.about.socials.donate\": \"Donate\",\n  \"settings.about.feedback.title\": \"Help us improve Certimate\",\n  \"settings.about.feedback.subtitle\": \"Tell us how to make Certimate work better for you.\",\n  \"settings.about.feedback.button\": \"Give feedback\",\n  \"settings.about.contributors.title\": \"Contributors\",\n  \"settings.about.contributors.tips\": \"Thanks to all the people who have contributed to this project.\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/en/nls.workflow.json",
    "content": "{\n  \"workflow.page.title\": \"Workflows\",\n  \"workflow.page.subtitle\": \"Workflows are collections of nodes that automate a process. Workflows begin execution when a trigger condition occurs and execute sequentially to achieve complex tasks.\",\n\n  \"workflow.nodata.title\": \"No workflows\",\n  \"workflow.nodata.description\": \"It looks like you don't have any workflows. Get started by adding one.\",\n  \"workflow.nodata.button\": \"Create workflow\",\n\n  \"workflow.search.placeholder\": \"Search by workflow name ...\",\n\n  \"workflow.action.create.button\": \"Create workflow\",\n  \"workflow.action.create.modal.title\": \"Create workflow\",\n  \"workflow.action.modify.menu\": \"Edit\",\n  \"workflow.action.duplicate.menu\": \"Duplicate\",\n  \"workflow.action.delete.menu\": \"Delete\",\n  \"workflow.action.delete.modal.title\": \"Delete \\\"{{name}}\\\"\",\n  \"workflow.action.delete.modal.content\": \"Are you sure want to delete this workflow? <br>This action cannot be undone.\",\n  \"workflow.action.batch_delete.modal.title\": \"Delete workflows\",\n  \"workflow.action.batch_delete.modal.content\": \"Are you sure want to delete these {{count}} selected workflows? <br>This action cannot be undone.\",\n  \"workflow.action.enable.button\": \"Activate\",\n  \"workflow.action.enable.errmsg.unpublished\": \"Please complete the orchestration and publish the changes first\",\n  \"workflow.action.disable.button\": \"Deactivate\",\n  \"workflow.action.execute.button\": \"Execute\",\n  \"workflow.action.execute.menu\": \"Execute\",\n  \"workflow.action.execute.modal.title\": \"Execute workflow\",\n  \"workflow.action.execute.modal.content\": \"You have unpublished changes. Do you really want to execute this workflow based on the last published version?\",\n  \"workflow.action.execute.prompt\": \"Running... Please check the history later\",\n\n  \"workflow.props.name\": \"Name\",\n  \"workflow.props.description\": \"Description\",\n  \"workflow.props.trigger\": \"Trigger\",\n  \"workflow.props.trigger.scheduled\": \"Scheduled\",\n  \"workflow.props.trigger.manual\": \"Manual\",\n  \"workflow.props.last_run_at\": \"Last run at\",\n  \"workflow.props.state\": \"Active\",\n  \"workflow.props.state.filter.all\": \"All\",\n  \"workflow.props.state.filter.enabled\": \"Active\",\n  \"workflow.props.state.filter.disabled\": \"Inactive\",\n  \"workflow.props.created_at\": \"Created at\",\n  \"workflow.props.updated_at\": \"Updated at\",\n\n  \"workflow.new.title\": \"Create Workflow\",\n  \"workflow.new.subtitle\": \"Using a workflow to monitor, apply, deploy and notify.\",\n  \"workflow.new.button.create\": \"Create from blank\",\n  \"workflow.new.button.import\": \"Import from file\",\n  \"workflow.new.templates.title\": \"Choose a Template\",\n  \"workflow.new.templates.subtitle\": \"Use these template workflows to quickly initialize your automated certificate management workflow.\",\n  \"workflow.new.templates.template.standard.title\": \"Standard template\",\n  \"workflow.new.templates.template.standard.description\": \"A standard operating procedure that includes application, deployment, and notification steps.\",\n  \"workflow.new.templates.template.certtest.title\": \"Monitoring template\",\n  \"workflow.new.templates.template.certtest.description\": \"A monitoring operating procedure that includes monitoring, and notification steps.\",\n  \"workflow.new.templates.default_name\": \"Untitled workflow\",\n  \"workflow.new.templates.default_description\": \"\",\n\n  \"workflow.detail.baseinfo.name.label\": \"Workflow name\",\n  \"workflow.detail.baseinfo.name.placeholder\": \"Please enter workflow name\",\n  \"workflow.detail.baseinfo.description.label\": \"Workflow description\",\n  \"workflow.detail.baseinfo.description.placeholder\": \"Please enter workflow description\",\n  \"workflow.detail.design.tab\": \"Orchestration\",\n  \"workflow.detail.design.editor.placeholder\": \"Please configure\",\n  \"workflow.detail.design.editor.add_node\": \"Add node\",\n  \"workflow.detail.design.editor.rename_node\": \"Rename node\",\n  \"workflow.detail.design.editor.duplicate_node\": \"Duplicate node\",\n  \"workflow.detail.design.editor.remove_node\": \"Delete node\",\n  \"workflow.detail.design.editor.add_branch\": \"Add branch\",\n  \"workflow.detail.design.editor.rename_branch\": \"Rename branch\",\n  \"workflow.detail.design.editor.duplicate_branch\": \"Duplicate branch\",\n  \"workflow.detail.design.editor.remove_branch\": \"Delete branch\",\n  \"workflow.detail.design.toolbar.zoomin\": \"Zoom in\",\n  \"workflow.detail.design.toolbar.zoomout\": \"Zoom out\",\n  \"workflow.detail.design.toolbar.auto_fit\": \"Auto fit view\",\n  \"workflow.detail.design.toolbar.horizontal_layout\": \"Horizontal Layout\",\n  \"workflow.detail.design.toolbar.vertical_layout\": \"Vertical Layout\",\n  \"workflow.detail.design.toolbar.minimap\": \"Minimap\",\n  \"workflow.detail.design.toolbar.drag_mode\": \"Drag mode\",\n  \"workflow.detail.design.toolbar.pointer_mode\": \"Pointer mode\",\n  \"workflow.detail.design.drawer.node_id.label\": \"Node ID: \",\n  \"workflow.detail.design.drawer.disabled.on.tooltip\": \"Disable\",\n  \"workflow.detail.design.drawer.disabled.off.tooltip\": \"Enable\",\n  \"workflow.detail.design.action.publish.button\": \"Publish\",\n  \"workflow.detail.design.action.publish.modal.title\": \"Publish changes\",\n  \"workflow.detail.design.action.publish.modal.content\": \"Are you sure to publish your changes?\",\n  \"workflow.detail.design.action.rollback.button\": \"Rollback\",\n  \"workflow.detail.design.action.rollback.modal.title\": \"Rollback changes\",\n  \"workflow.detail.design.action.rollback.modal.content\": \"Are you sure to rollback your changes to the last published version?\",\n  \"workflow.detail.design.action.import.button\": \"Import\",\n  \"workflow.detail.design.action.import.modal.title\": \"Import workflow\",\n  \"workflow.detail.design.action.import.modal.ok_button\": \"Import\",\n  \"workflow.detail.design.action.import.form.format.label\": \"Format\",\n  \"workflow.detail.design.action.import.form.content.label\": \"Content\",\n  \"workflow.detail.design.action.import.form.content.errmsg.invalid\": \"Please enter a valid content\",\n  \"workflow.detail.design.action.import.form.content.errmsg.first_node_start\": \"The first node must be a start node\",\n  \"workflow.detail.design.action.import.form.content.errmsg.last_node_end\": \"The last node must be an end node\",\n  \"workflow.detail.design.action.import.form.content.errmsg.duplicate_start\": \"The start node must be unique\",\n  \"workflow.detail.design.action.import.form.content.errmsg.invalid_id\": \"Node ID invalid: #{{nodeId}}\",\n  \"workflow.detail.design.action.import.form.content.errmsg.invalid_config\": \"Node config invalid: #{{nodeId}}\",\n  \"workflow.detail.design.action.import.form.content.errmsg.conflict_id\": \"Node ID conflict: #{{nodeId}}\",\n  \"workflow.detail.design.action.import.form.content.errmsg.abnormal_condition_branch\": \"Abnormal parallel/conditional branch: #{{nodeId}}\",\n  \"workflow.detail.design.action.import.form.content.errmsg.abnormal_try_catch_branch\": \"Abnormal execution result branch: #{{nodeId}}\",\n  \"workflow.detail.design.action.export.button\": \"Export\",\n  \"workflow.detail.design.action.export.modal.title\": \"Export workflow\",\n  \"workflow.detail.design.action.export.form.format.label\": \"Format\",\n  \"workflow.detail.design.action.export.form.content.label\": \"Content\",\n  \"workflow.detail.design.uncompleted_design.alert\": \"The orchestration is not ready. Please check if there are any nodes not configured.\",\n  \"workflow.detail.design.unpublished_draft.alert\": \"The orchestration is not released yet.\",\n  \"workflow.detail.design.unsaved_changes.confirm\": \"You have unsaved changes. Do you really want to close the panel and drop those changes?\",\n  \"workflow.detail.runs.tab\": \"History runs\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/en/nls.workflow.nodes.json",
    "content": "{\r\n  \"workflow_node.kind.basis\": \"Basis\",\r\n  \"workflow_node.kind.business\": \"Business\",\r\n  \"workflow_node.kind.logic\": \"Logic\",\r\n\r\n  \"workflow_node.start.label\": \"Start\",\r\n  \"workflow_node.start.default_name\": \"Start\",\r\n  \"workflow_node.start.form_anchor.parameters.tab\": \"Parameters\",\r\n  \"workflow_node.start.form.trigger.label\": \"Trigger\",\r\n  \"workflow_node.start.form.trigger.placeholder\": \"Please select trigger\",\r\n  \"workflow_node.start.form.trigger.option.scheduled.label\": \"Scheduled\",\r\n  \"workflow_node.start.form.trigger.option.manual.label\": \"Manual\",\r\n  \"workflow_node.start.form.trigger_cron.label\": \"CRON expression\",\r\n  \"workflow_node.start.form.trigger_cron.placeholder\": \"Please enter CRON expression\",\r\n  \"workflow_node.start.form.trigger_cron.errmsg.invalid\": \"Please enter a valid CRON expression\",\r\n  \"workflow_node.start.form.trigger_cron.tooltip\": \"Exactly 5 space separated segments, in standard <em>crontab</em> rules.\",\r\n  \"workflow_node.start.form.trigger_cron.help\": \"Expected execution time for the last 5 times (the actual time zone is based on the server):\",\r\n  \"workflow_node.start.form.trigger_cron.guide\": \"If you have multiple workflows, it is recommended to set them to run at different times of the day instead of always running at a specific time. And please don't always set it to midnight every day to avoid spikes in traffic. <br><br>Reference links:<br>1. <a href=\\\"https://letsencrypt.org/docs/rate-limits/\\\" target=\\\"_blank\\\">Let’s Encrypt rate limits</a><br>2. <a href=\\\"https://letsencrypt.org/docs/faq/#why-should-my-let-s-encrypt-acme-client-run-at-a-random-time\\\" target=\\\"_blank\\\">Why should my Let’s Encrypt (ACME) client run at a random time?</a>\",\r\n\r\n  \"workflow_node.apply.label\": \"Request certificate\",\r\n  \"workflow_node.apply.default_name\": \"Application\",\r\n  \"workflow_node.apply.form_anchor.parameters.tab\": \"Parameters\",\r\n  \"workflow_node.apply.form_anchor.challenge.tab\": \"Challenge\",\r\n  \"workflow_node.apply.form_anchor.challenge.title\": \"Challenge validation\",\r\n  \"workflow_node.apply.form_anchor.certificate.tab\": \"Certificate\",\r\n  \"workflow_node.apply.form_anchor.certificate.title\": \"Certificate settings\",\r\n  \"workflow_node.apply.form_anchor.advanced.tab\": \"Advanced\",\r\n  \"workflow_node.apply.form_anchor.advanced.title\": \"Advanced settings\",\r\n  \"workflow_node.apply.form_anchor.strategy.tab\": \"Strategy\",\r\n  \"workflow_node.apply.form_anchor.strategy.title\": \"Strategy settings\",\r\n  \"workflow_node.apply.form.identifier.label\": \"Identifier type\",\r\n  \"workflow_node.apply.form.identifier.label2\": \"The certificate is issued for ...\",\r\n  \"workflow_node.apply.form.identifier.option.domain.label\": \"Domains\",\r\n  \"workflow_node.apply.form.identifier.option.domain.description\": \"<ul style=\\\"margin: 0;\\\"><li>Supports single-domain, multi-domain and wildcard domains (Depends on CA).</li><li>Supports DNS-01 or HTTP-01 challenge.</li></ul>\",\r\n  \"workflow_node.apply.form.identifier.option.ip.label\": \"IP addresses\",\r\n  \"workflow_node.apply.form.identifier.option.ip.description\": \"<ul style=\\\"margin: 0;\\\"><li>Supports IPv4 and IPv6 addresses.</li><li>Only supports HTTP-01 challenge.</li><li>Default configuration: No common name.</li><li>Default configuration: CA is <em><b>Let's Encrypt</b></em>.</li><li>Default configuration: ACME profile is <em><b>shortlived</b></em>.</li><li>Shorter certificate validity and renewal interval.</li></ul>\",\r\n  \"workflow_node.apply.form.identifier.continue.button\": \"Continue\",\r\n  \"workflow_node.apply.form.domains.label\": \"Domains\",\r\n  \"workflow_node.apply.form.domains.placeholder\": \"Please enter domains (separated by semicolons)\",\r\n  \"workflow_node.apply.form.domains.help\": \"Notes: Multiple domains should be separated by semicolons. Wildcard domain should be written as <em>*.example.com</em>.\",\r\n  \"workflow_node.apply.form.domains.help_no_wildcard\": \"Notes: Multiple domains should be separated by semicolons.\",\r\n  \"workflow_node.apply.form.domains.multiple_input_modal.title\": \"Change domains\",\r\n  \"workflow_node.apply.form.domains.multiple_input_modal.placeholder\": \"Please enter domain\",\r\n  \"workflow_node.apply.form.ipaddrs.label\": \"IP addresses\",\r\n  \"workflow_node.apply.form.ipaddrs.placeholder\": \"Please enter IP addresses (separated by semicolons)\",\r\n  \"workflow_node.apply.form.ipaddrs.help\": \"Notes: Multiple IP addresses should be separated by semicolons.\",\r\n  \"workflow_node.apply.form.ipaddrs.multiple_input_modal.title\": \"Change IP addresses\",\r\n  \"workflow_node.apply.form.ipaddrs.multiple_input_modal.placeholder\": \"Please enter IP address\",\r\n  \"workflow_node.apply.form.contact_email.label\": \"Contact email\",\r\n  \"workflow_node.apply.form.contact_email.placeholder\": \"Please enter contact email\",\r\n  \"workflow_node.apply.form.contact_email.tooltip\": \"Contact information required for SSL certificate application. Please pay attention to the <a href=\\\"https://letsencrypt.org/docs/rate-limits/\\\" target=\\\"_blank\\\">rate limits</a>.\",\r\n  \"workflow_node.apply.form.challenge_type.label\": \"Challenge type\",\r\n  \"workflow_node.apply.form.challenge_type.placeholder\": \"Please select challenge type\",\r\n  \"workflow_node.apply.form.challenge_type.errmsg.no_wildcard_in_http01\": \"Could not use HTTP-01 challenge to request wildcard domain certificates.\",\r\n  \"workflow_node.apply.form.challenge_type.errmsg.no_ip_in_dns01\": \"Could not use DNS-01 challenge to request IP address certificates.\",\r\n  \"workflow_node.apply.form.challenge_type.tooltip\": \"It determines how the CAs verifies your control over the domain names. <br><a href=\\\"https://letsencrypt.org/docs/challenge-types/\\\" target=\\\"_blank\\\">Click here to learn more</a>.\",\r\n  \"workflow_node.apply.form.provider.label\": \"Provider\",\r\n  \"workflow_node.apply.form.provider.placeholder\": \"Please select provider\",\r\n  \"workflow_node.apply.form.provider_dns01.label\": \"DNS provider\",\r\n  \"workflow_node.apply.form.provider_dns01.placeholder\": \"Please select the DNS provider of the domains\",\r\n  \"workflow_node.apply.form.provider_http01.label\": \"Hosting provider\",\r\n  \"workflow_node.apply.form.provider_http01.placeholder\": \"Please select the hosting provider of the domains\",\r\n  \"workflow_node.apply.form.provider_access.label\": \"Provider credential\",\r\n  \"workflow_node.apply.form.provider_access.placeholder\": \"Please select an credential of provider\",\r\n  \"workflow_node.apply.form.provider_access.button\": \"Create\",\r\n  \"workflow_node.apply.form.provider_access_dns01.label\": \"DNS provider credential\",\r\n  \"workflow_node.apply.form.provider_access_dns01.placeholder\": \"Please select an credential of DNS provider\",\r\n  \"workflow_node.apply.form.provider_access_http01.label\": \"Hosting provider credential\",\r\n  \"workflow_node.apply.form.provider_access_http01.placeholder\": \"Please select an credential of hosting provider\",\r\n  \"workflow_node.apply.form.aliyun_esa_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.apply.form.aliyun_esa_region.placeholder\": \"Please enter Alibaba Cloud ESA region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.apply.form.aliyun_esa_region.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint</a>\",\r\n  \"workflow_node.apply.form.aws_route53_region.label\": \"AWS Region\",\r\n  \"workflow_node.apply.form.aws_route53_region.placeholder\": \"Please enter AWS Route53 region (e.g. us-east-1)\",\r\n  \"workflow_node.apply.form.aws_route53_region.tooltip\": \"For more information, see <a href=\\\"https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints</a>\",\r\n  \"workflow_node.apply.form.aws_route53_hosted_zone_id.label\": \"AWS Route53 hosted zone ID (Optional)\",\r\n  \"workflow_node.apply.form.aws_route53_hosted_zone_id.placeholder\": \"Please enter AWS Route53 hosted zone ID\",\r\n  \"workflow_node.apply.form.aws_route53_hosted_zone_id.help\": \"Notes: Only required when there are several hosted zones with the same FQDN.\",\r\n  \"workflow_node.apply.form.aws_route53_hosted_zone_id.tooltip\": \"For more information, see <a href=\\\"https://docs.aws.amazon.com/en_us/Route53/latest/DeveloperGuide/hosted-zones-working-with.html\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/en_us/Route53/latest/DeveloperGuide/hosted-zones-working-with.html</a>\",\r\n  \"workflow_node.apply.form.huaweicloud_dns_region.label\": \"Huawei Cloud region\",\r\n  \"workflow_node.apply.form.huaweicloud_dns_region.placeholder\": \"Please enter Huawei Cloud DNS region (e.g. cn-north-1)\",\r\n  \"workflow_node.apply.form.huaweicloud_dns_region.tooltip\": \"For more information, see <a href=\\\"https://console-intl.huaweicloud.com/apiexplorer/#/endpoint?locale=en-us\\\" target=\\\"_blank\\\">https://console-intl.huaweicloud.com/apiexplorer/#/endpoint</a>\",\r\n  \"workflow_node.apply.form.jdcloud_dns_region_id.label\": \"JD Cloud region ID\",\r\n  \"workflow_node.apply.form.jdcloud_dns_region_id.placeholder\": \"Please enter JD Cloud DNS region ID (e.g. cn-north-1)\",\r\n  \"workflow_node.apply.form.jdcloud_dns_region_id.tooltip\": \"For more information, see <a href=\\\"https://docs.jdcloud.com/en/common-declaration/api/introduction\\\" target=\\\"_blank\\\">https://docs.jdcloud.com/en/common-declaration/api/introduction</a>\",\r\n  \"workflow_node.apply.form.s3_region.label\": \"Object storage (S3-compatible) region\",\r\n  \"workflow_node.apply.form.s3_region.placeholder\": \"Please enter region\",\r\n  \"workflow_node.apply.form.s3_bucket.label\": \"Object storage (S3-compatible) bucket\",\r\n  \"workflow_node.apply.form.s3_bucket.placeholder\": \"Please enter bucket name\",\r\n  \"workflow_node.apply.form.local_webroot_path.label\": \"Web root path\",\r\n  \"workflow_node.apply.form.local_webroot_path.placeholder\": \"Please enter web root path\",\r\n  \"workflow_node.apply.form.local_webroot_path.tooltip\": \"It's the main directory where the website's files are stored on the server.\",\r\n  \"workflow_node.apply.form.ssh_webroot_path.label\": \"Web root path\",\r\n  \"workflow_node.apply.form.ssh_webroot_path.placeholder\": \"Please enter web root path\",\r\n  \"workflow_node.apply.form.ssh_webroot_path.tooltip\": \"It's the main directory where the website's files are stored on the server.\",\r\n  \"workflow_node.apply.form.ssh_use_scp.label\": \"Fallback to use SCP\",\r\n  \"workflow_node.apply.form.ssh_use_scp.tooltip\": \"If the remote server does not support SFTP, please check this option to fallback to SCP.\",\r\n  \"workflow_node.apply.form.key_source.label\": \"Key source\",\r\n  \"workflow_node.apply.form.key_source.placeholder\": \"Please select key source\",\r\n  \"workflow_node.apply.form.key_source.option.auto.label\": \"Auto\",\r\n  \"workflow_node.apply.form.key_source.option.reuse.label\": \"Reuse\",\r\n  \"workflow_node.apply.form.key_source.option.custom.label\": \"Custom\",\r\n  \"workflow_node.apply.form.key_algorithm.label\": \"Key algorithm\",\r\n  \"workflow_node.apply.form.key_algorithm.placeholder\": \"Please select key algorithm\",\r\n  \"workflow_node.apply.form.key_algorithm.help_reuse\": \"Notes: If there is an existing certificate, the original key algorithm will be used.\",\r\n  \"workflow_node.apply.form.key_algorithm.help_custom\": \"Notes: Please ensure that the algorithm matches the private key.\",\r\n  \"workflow_node.apply.form.key_content.label\": \"Private key (PEM format)\",\r\n  \"workflow_node.apply.form.key_content.placeholder\": \"-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----\",\r\n  \"workflow_node.apply.form.key_content.errmsg.invalid\": \"Please enter a valid PEM private key\",\r\n  \"workflow_node.apply.form.key_content.errmsg.not_matched\": \"Private key does not match the key algorithm (Expected: {{expected}}, Actual: {{actual}})\",\r\n  \"workflow_node.apply.form.ca_provider.label\": \"Certificate authority (Optional)\",\r\n  \"workflow_node.apply.form.ca_provider.placeholder\": \"Please select a certificate authority\",\r\n  \"workflow_node.apply.form.ca_provider.tooltip\": \"Used to issue SSL certificates.\",\r\n  \"workflow_node.apply.form.ca_provider.button\": \"Configure\",\r\n  \"workflow_node.apply.form.ca_provider_access.label\": \"Certificate authority credential\",\r\n  \"workflow_node.apply.form.ca_provider_access.placeholder\": \"Please select an credential of the certificate authority\",\r\n  \"workflow_node.apply.form.ca_provider_access.button\": \"Create\",\r\n  \"workflow_node.apply.form.validity_lifetime.label\": \"Validity lifetime (Optional)\",\r\n  \"workflow_node.apply.form.validity_lifetime.placeholder\": \"Please enter certificate's validity lifetime\",\r\n  \"workflow_node.apply.form.validity_lifetime.help\": \"Notes: Not all CAs support this feature.\",\r\n  \"workflow_node.apply.form.validity_lifetime.tooltip\": \"It determines the <em>NotAfter</em> field of the certificate in the ACME protocol. If you don't understand this option, just keep it by default.\",\r\n  \"workflow_node.apply.form.validity_lifetime.units.h\": \"hours\",\r\n  \"workflow_node.apply.form.validity_lifetime.units.d\": \"days\",\r\n  \"workflow_node.apply.form.preferred_chain.label\": \"Preferred chain (Optional)\",\r\n  \"workflow_node.apply.form.preferred_chain.placeholder\": \"Please enter preferred chain\",\r\n  \"workflow_node.apply.form.preferred_chain.help\": \"Notes: Not all CAs support this feature.\",\r\n  \"workflow_node.apply.form.preferred_chain.tooltip\": \"It determines the <em>PreferredChain</em> field of the certificate in the ACME protocol. If you don't understand this option, just keep it by default. <br><a href=\\\"https://letsencrypt.org/certificates/\\\" target=\\\"_blank\\\">Click here to learn more</a>.\",\r\n  \"workflow_node.apply.form.acme_profile.label\": \"ACME profile (Optional)\",\r\n  \"workflow_node.apply.form.acme_profile.placeholder\": \"Please enter ACME profile\",\r\n  \"workflow_node.apply.form.acme_profile.help\": \"Notes: Not all CAs support this feature.\",\r\n  \"workflow_node.apply.form.acme_profile.tooltip\": \"It determines the <em>Profile</em> field of the certificate in the ACME protocol. If you don't understand this option, just keep it by default. <br><a href=\\\"https://letsencrypt.org/docs/profiles/#our-profiles\\\" target=\\\"_blank\\\">Click here to learn more</a>.\",\r\n  \"workflow_node.apply.form.disable_cn.label\": \"Disable common name in CSR\",\r\n  \"workflow_node.apply.form.disable_cn.tooltip\": \"It determines whether to include the <em>Subject.CommonName</em> field in the certificate. If you don't understand this option, just keep it by default. <br><a href=\\\"https://letsencrypt.org/docs/profiles/#certificate-common-name\\\" target=\\\"_blank\\\">Click here to learn more</a>.\",\r\n  \"workflow_node.apply.form.nameservers.label\": \"DNS recursive nameservers (Optional)\",\r\n  \"workflow_node.apply.form.nameservers.placeholder\": \"Please enter DNS recursive nameservers (separated by semicolons)\",\r\n  \"workflow_node.apply.form.nameservers.tooltip\": \"It determines whether to custom DNS recursive nameservers during ACME challenge. If you don't understand this option, just keep it by default. <br><a href=\\\"https://go-acme.github.io/lego/usage/cli/options/index.html#dns-resolvers-and-challenge-verification\\\" target=\\\"_blank\\\">Click here to learn more</a>.\",\r\n  \"workflow_node.apply.form.nameservers.multiple_input_modal.title\": \"Change DNS rcursive nameservers\",\r\n  \"workflow_node.apply.form.nameservers.multiple_input_modal.placeholder\": \"Please enter DNS recursive nameserver\",\r\n  \"workflow_node.apply.form.dns_propagation_wait.label\": \"DNS propagation waiting time (Optional)\",\r\n  \"workflow_node.apply.form.dns_propagation_wait.placeholder\": \"Please enter DNS propagation waiting time\",\r\n  \"workflow_node.apply.form.dns_propagation_wait.unit\": \"seconds\",\r\n  \"workflow_node.apply.form.dns_propagation_wait.tooltip\": \"It determines the waiting time for DNS propagation during ACME DNS-01 challenge. If you don't understand this option, just keep it by default.\",\r\n  \"workflow_node.apply.form.dns_propagation_timeout.label\": \"DNS propagation checks timeout (Optional)\",\r\n  \"workflow_node.apply.form.dns_propagation_timeout.placeholder\": \"Please enter DNS propagation checks timeout\",\r\n  \"workflow_node.apply.form.dns_propagation_timeout.unit\": \"seconds\",\r\n  \"workflow_node.apply.form.dns_propagation_timeout.tooltip\": \"It determines the timeout for DNS propagation checks during ACME DNS-01 challenge. If you don't understand this option, just keep it by default.\",\r\n  \"workflow_node.apply.form.dns_ttl.label\": \"DNS TTL (Optional)\",\r\n  \"workflow_node.apply.form.dns_ttl.placeholder\": \"Please enter DNS TTL\",\r\n  \"workflow_node.apply.form.dns_ttl.unit\": \"seconds\",\r\n  \"workflow_node.apply.form.dns_ttl.help\": \"Notes: Leave it blank to use the default value provided by the DNS provider.\",\r\n  \"workflow_node.apply.form.dns_ttl.tooltip\": \"It determines the TTL for DNS record during ACME DNS-01 challenge. If you don't understand this option, just keep it by default.\",\r\n  \"workflow_node.apply.form.http_delay_wait.label\": \"HTTP delay waiting time (Optional)\",\r\n  \"workflow_node.apply.form.http_delay_wait.placeholder\": \"Please enter HTTP delay waiting time\",\r\n  \"workflow_node.apply.form.http_delay_wait.unit\": \"seconds\",\r\n  \"workflow_node.apply.form.http_delay_wait.tooltip\": \"It determines a delay between the start of the HTTP server and the challenge validation during ACME HTTP-01 challenge. If you don't understand this option, just keep it by default.\",\r\n  \"workflow_node.apply.form.disable_follow_cname.label\": \"Disable CNAME following\",\r\n  \"workflow_node.apply.form.disable_follow_cname.tooltip\": \"It determines whether to disable CNAME following during ACME DNS-01 challenge. If you don't understand this option, just keep it by default. <br><a href=\\\"https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme/#the-advantages-of-a-cname\\\" target=\\\"_blank\\\">Click here to learn more</a>.\",\r\n  \"workflow_node.apply.form.disable_ari.label\": \"Disable ARI\",\r\n  \"workflow_node.apply.form.disable_ari.tooltip\": \"It determines whether to disable ARI (ACME Renewal Information). If you don't understand this option, just keep it by default. <br><a href=\\\"https://letsencrypt.org/2023/03/23/improving-resliiency-and-reliability-with-ari/\\\" target=\\\"_blank\\\">Click here to learn more</a>.\",\r\n  \"workflow_node.apply.form.skip_before_expiry_days.label\": \"Repeated application\",\r\n  \"workflow_node.apply.form.skip_before_expiry_days.placeholder\": \"Please enter renewal interval\",\r\n  \"workflow_node.apply.form.skip_before_expiry_days.prefix\": \"If the last certificate expiration time exceeds\",\r\n  \"workflow_node.apply.form.skip_before_expiry_days.suffix\": \", skip to re-apply.\",\r\n  \"workflow_node.apply.form.skip_before_expiry_days.unit\": \"days\",\r\n\r\n  \"workflow_node.upload.label\": \"Upload certificate\",\r\n  \"workflow_node.upload.default_name\": \"Uploading\",\r\n  \"workflow_node.upload.form_anchor.parameters.tab\": \"Parameters\",\r\n  \"workflow_node.upload.form.guide\": \"The file content will be read again every time this node executes.\",\r\n  \"workflow_node.upload.form.source.label\": \"Upload source\",\r\n  \"workflow_node.upload.form.source.placeholder\": \"Please select upload source\",\r\n  \"workflow_node.upload.form.source.option.form.label\": \"Form\",\r\n  \"workflow_node.upload.form.source.option.local.label\": \"Local path\",\r\n  \"workflow_node.upload.form.source.option.url.label\": \"URL\",\r\n  \"workflow_node.upload.form.name.label\": \"Domains / IP addresses\",\r\n  \"workflow_node.upload.form.name.placholder\": \"Please select certificate file\",\r\n  \"workflow_node.upload.form.certificate_pem.label\": \"Certificate (PEM format)\",\r\n  \"workflow_node.upload.form.certificate_pem.placeholder\": \"-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----\",\r\n  \"workflow_node.upload.form.certificate_pem.errmsg.invalid\": \"Please enter a valid PEM certificate\",\r\n  \"workflow_node.upload.form.certificate_path.label\": \"Certificate file path\",\r\n  \"workflow_node.upload.form.certificate_path.placeholder\": \"Please enter the local path for certificate file\",\r\n  \"workflow_node.upload.form.certificate_url.label\": \"Certificate file URL\",\r\n  \"workflow_node.upload.form.certificate_url.placeholder\": \"Please enter the URL for downloading certificate file\",\r\n  \"workflow_node.upload.form.private_key_pem.label\": \"Private key (PEM format)\",\r\n  \"workflow_node.upload.form.private_key_pem.placeholder\": \"-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----\",\r\n  \"workflow_node.upload.form.private_key_pem.errmsg.invalid\": \"Please enter a valid PEM private key\",\r\n  \"workflow_node.upload.form.private_key_path.label\": \"Private key file path\",\r\n  \"workflow_node.upload.form.private_key_path.placeholder\": \"Please enter the local path for private key file\",\r\n  \"workflow_node.upload.form.private_key_url.label\": \"Private key file URL\",\r\n  \"workflow_node.upload.form.private_key_url.placeholder\": \"Please enter the URL for downloading private key file\",\r\n\r\n  \"workflow_node.monitor.label\": \"Monitor certificate\",\r\n  \"workflow_node.monitor.default_name\": \"Monitoring\",\r\n  \"workflow_node.monitor.form_anchor.parameters.tab\": \"Parameters\",\r\n  \"workflow_node.monitor.form.guide\": \"It will send a HEAD request to the target address to obtain the certificate. Please ensure that the address is accessible through HTTPS protocol.\",\r\n  \"workflow_node.monitor.form.host.label\": \"Host\",\r\n  \"workflow_node.monitor.form.host.placeholder\": \"Please enter host\",\r\n  \"workflow_node.monitor.form.port.label\": \"Port\",\r\n  \"workflow_node.monitor.form.port.placeholder\": \"Please enter port\",\r\n  \"workflow_node.monitor.form.domain.label\": \"Domain (Optional)\",\r\n  \"workflow_node.monitor.form.domain.placeholder\": \"Please enter domain name\",\r\n  \"workflow_node.monitor.form.domain.help\": \"Notes: Only required when the host is an IP address.\",\r\n  \"workflow_node.monitor.form.request_path.label\": \"Request path (Optional)\",\r\n  \"workflow_node.monitor.form.request_path.placeholder\": \"Please enter request path\",\r\n\r\n  \"workflow_node.deploy.label\": \"Deploy certificate\",\r\n  \"workflow_node.deploy.default_name\": \"Deployment\",\r\n  \"workflow_node.deploy.form_anchor.parameters.tab\": \"Parameters\",\r\n  \"workflow_node.deploy.form_anchor.deployment.tab\": \"Deployment\",\r\n  \"workflow_node.deploy.form_anchor.deployment.title\": \"Deployment settings\",\r\n  \"workflow_node.deploy.form_anchor.strategy.tab\": \"Strategy\",\r\n  \"workflow_node.deploy.form_anchor.strategy.title\": \"Strategy settings\",\r\n  \"workflow_node.deploy.form.certificate_output_node_id.label\": \"Certificate to deploy\",\r\n  \"workflow_node.deploy.form.certificate_output_node_id.placeholder\": \"Please select certificate to deploy\",\r\n  \"workflow_node.deploy.form.certificate_output_node_id.help\": \"Notes: The certificate to be deployed comes from the previous nodes of application or upload.\",\r\n  \"workflow_node.deploy.form.provider.label\": \"Deployment target\",\r\n  \"workflow_node.deploy.form.provider.placeholder\": \"Please select deployment target\",\r\n  \"workflow_node.deploy.form.provider.search.placeholder\": \"Search deployment target ...\",\r\n  \"workflow_node.deploy.form.provider_access.label\": \"Hosting provider credential\",\r\n  \"workflow_node.deploy.form.provider_access.placeholder\": \"Please select an credential of hosting provider\",\r\n  \"workflow_node.deploy.form.provider_access.button\": \"Create\",\r\n  \"workflow_node.deploy.form.shared_resource_type.label\": \"Resource type\",\r\n  \"workflow_node.deploy.form.shared_resource_type.placeholder\": \"Please select resource type\",\r\n  \"workflow_node.deploy.form.shared_domain_match_pattern.label\": \"Domain match pattern\",\r\n  \"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\": \"Please select domain match pattern\",\r\n  \"workflow_node.deploy.form.shared_domain_match_pattern.option.exact.label\": \"Exact matches\",\r\n  \"workflow_node.deploy.form.shared_domain_match_pattern.option.wildcard.label\": \"Wildcard matches\",\r\n  \"workflow_node.deploy.form.shared_domain_match_pattern.option.certsan.label\": \"via Certificate\",\r\n  \"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\": \"Notes: For the sites which support wildcard resolution, an <strong>exact match</strong> of a wildcard domain only includes the site itself, does not include its subdomains.\",\r\n  \"workflow_node.deploy.form.shared_script_command.vartips\": \"Supported variables: <br><ol style=\\\"list-style: disc;\\\"><li><strong>${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}</strong>: <br>The path of the certificate file, same as the value of the form related field.</li><li><strong>${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}</strong>: <br>The path of the server certificate file, same as the value of the form related field.</li><li><strong>${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_INTERMEDIA_PATH}</strong>: <br>The path of the intermediate CA certificate file, same as the value of the form related field.</li><li><strong>${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}</strong>: <br>The path of the private key file, same as the value of the form related field.</li><li><strong>${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}</strong>: <br>The PFX password, same as the value of the form related field.</li><li><strong>${CERTIMATE_DEPLOYER_CMDVAR_JKS_ALIAS}</strong>: <br>The JKS alias, same as the value of the form related field.</li><li><strong>${CERTIMATE_DEPLOYER_CMDVAR_JKS_KEYPASS}</strong>: <br>The JKS key password, same as the value of the form related field.</li><li><strong>${CERTIMATE_DEPLOYER_CMDVAR_JKS_STOREPASS}</strong>: <br>The JKS store password, same as the value of the form related field.</li></ol>\",\r\n  \"workflow_node.deploy.form.1panel_node_name.label\": \"1Panel node name (Optional)\",\r\n  \"workflow_node.deploy.form.1panel_node_name.placeholder\": \"Please enter 1Panel node name\",\r\n  \"workflow_node.deploy.form.1panel_node_name.help\": \"Notes: Only used for 1Panel v2+.\",\r\n  \"workflow_node.deploy.form.1panel_node_name.tooltip\": \"You can find it on 1Panel dashboard.\",\r\n  \"workflow_node.deploy.form.1panel_resource_type.option.website.label\": \"Website\",\r\n  \"workflow_node.deploy.form.1panel_resource_type.option.certificate.label\": \"Certificate\",\r\n  \"workflow_node.deploy.form.1panel_website_match_pattern.label\": \"Website match pattern\",\r\n  \"workflow_node.deploy.form.1panel_website_match_pattern.placeholder\": \"Please select website match pattern\",\r\n  \"workflow_node.deploy.form.1panel_website_match_pattern.option.specified.label\": \"Specified ID\",\r\n  \"workflow_node.deploy.form.1panel_website_match_pattern.option.certsan.label\": \"via Certificate\",\r\n  \"workflow_node.deploy.form.1panel_website_match_pattern.help_certsan\": \"Notes: The website name should be a domain name and include SSL configurations.\",\r\n  \"workflow_node.deploy.form.1panel_website_id.label\": \"1Panel website ID\",\r\n  \"workflow_node.deploy.form.1panel_website_id.placeholder\": \"Please enter 1Panel website ID\",\r\n  \"workflow_node.deploy.form.1panel_website_id.tooltip\": \"You can find it on 1Panel dashboard.\",\r\n  \"workflow_node.deploy.form.1panel_certificate_id.label\": \"1Panel certificate ID\",\r\n  \"workflow_node.deploy.form.1panel_certificate_id.placeholder\": \"Please enter 1Panel certificate ID\",\r\n  \"workflow_node.deploy.form.1panel_certificate_id.tooltip\": \"You can find it on 1Panel dashboard.\",\r\n  \"workflow_node.deploy.form.1panel_console_auto_restart.label\": \"Auto restart 1Panel after deployment\",\r\n  \"workflow_node.deploy.form.aliyun_alb_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.deploy.form.aliyun_alb_region.placeholder\": \"Please enter Alibaba Cloud ALB region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.deploy.form.aliyun_alb_region.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/slb/application-load-balancer/product-overview/supported-regions-and-zones\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/slb/application-load-balancer/product-overview/supported-regions-and-zones</a>\",\r\n  \"workflow_node.deploy.form.aliyun_alb_resource_type.option.loadbalancer.label\": \"ALB load balancer\",\r\n  \"workflow_node.deploy.form.aliyun_alb_resource_type.option.listener.label\": \"ALB listener\",\r\n  \"workflow_node.deploy.form.aliyun_alb_loadbalancer_id.label\": \"Alibaba Cloud ALB load balancer ID\",\r\n  \"workflow_node.deploy.form.aliyun_alb_loadbalancer_id.placeholder\": \"Please enter Alibaba Cloud ALB load balancer ID\",\r\n  \"workflow_node.deploy.form.aliyun_alb_loadbalancer_id.tooltip\": \"For more information, see <a href=\\\"https://slb.console.aliyun.com/alb\\\" target=\\\"_blank\\\">https://slb.console.aliyun.com/alb</a>\",\r\n  \"workflow_node.deploy.form.aliyun_alb_listener_id.label\": \"Alibaba Cloud ALB listener ID\",\r\n  \"workflow_node.deploy.form.aliyun_alb_listener_id.placeholder\": \"Please enter Alibaba Cloud ALB listener ID\",\r\n  \"workflow_node.deploy.form.aliyun_alb_listener_id.tooltip\": \"For more information, see <a href=\\\"https://slb.console.aliyun.com/alb\\\" target=\\\"_blank\\\">https://slb.console.aliyun.com/alb</a>\",\r\n  \"workflow_node.deploy.form.aliyun_alb_snidomain.label\": \"Alibaba Cloud ALB SNI domain (Optional)\",\r\n  \"workflow_node.deploy.form.aliyun_alb_snidomain.placeholder\": \"Please enter Alibaba Cloud ALB SNI domain name\",\r\n  \"workflow_node.deploy.form.aliyun_alb_snidomain.help\": \"Notes: Leave it blank to set the default certificate; otherwise, to set the extension one for SNI.\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_region.placeholder\": \"Please enter Alibaba Cloud API gateway region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_region.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/api-gateway/cloud-native-api-gateway/product-overview/regions\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/api-gateway/cloud-native-api-gateway/product-overview/regions</a>\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_service_type.label\": \"Alibaba Cloud API gateway type\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_service_type.placeholder\": \"Please select Alibaba Cloud API gateway type\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_service_type.option.cloudnative.label\": \"Cloud-native API gateway\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_service_type.option.traditional.label\": \"Traditional API gateway\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_gateway_id.label\": \"Alibaba Cloud API gateway ID\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_gateway_id.placeholder\": \"Please enter Alibaba Cloud API gateway ID\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_gateway_id.tooltip\": \"For more information, see <a href=\\\"https://apigw.console.aliyun.com\\\" target=\\\"_blank\\\">https://apigw.console.aliyun.com</a>\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_group_id.label\": \"Alibaba Cloud API group ID\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_group_id.placeholder\": \"Please enter Alibaba Cloud API group ID\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_group_id.tooltip\": \"For more information, see <a href=\\\"https://apigateway.console.aliyun.com\\\" target=\\\"_blank\\\">https://apigateway.console.aliyun.com</a>\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_domain.label\": \"Alibaba Cloud API gateway domain\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_domain.placeholder\": \"Please enter Alibaba Cloud API gateway domain\",\r\n  \"workflow_node.deploy.form.aliyun_cas_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.deploy.form.aliyun_cas_region.placeholder\": \"Please enter Alibaba Cloud CAS region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.deploy.form.aliyun_cas_region.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/ssl-certificate/developer-reference/endpoints\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/ssl-certificate/developer-reference/endpoints</a>\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy.guide\": \"TIPS: This will invoke Alibaba Cloud OpenAPI <em>CreateDeploymentJob</em> to create an asynchronously deployment task. You need to go to the Alibaba Cloud console to check the actual deployment results by yourself.\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_region.placeholder\": \"Please enter Alibaba Cloud CAS region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_region.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/ssl-certificate/developer-reference/endpoints\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/ssl-certificate/developer-reference/endpoints</a>\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.label\": \"Alibaba Cloud resource IDs\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.placeholder\": \"Please enter Alibaba Cloud resource IDs (separated by semicolons)\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.errmsg.invalid\": \"Please enter a valid Alibaba Cloud resource ID\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.help\": \"Notes: Multiple values should be separated by semicolons. Only Alibaba Cloud products are supported.\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/ssl-certificate/developer-reference/api-cas-2020-04-07-listcloudresources\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/ssl-certificate/developer-reference/api-cas-2020-04-07-listcloudresources</a>\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.multiple_input_modal.title\": \"Change Alibaba Cloud resource IDs\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.multiple_input_modal.placeholder\": \"Please enter Alibaba Cloud resouce ID\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.label\": \"Alibaba Cloud contact IDs (Optional)\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.placeholder\": \"Please enter Alibaba Cloud contact IDs (separated by semicolons)\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.errmsg.invalid\": \"Please enter a valid Alibaba Cloud contact ID\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.help\": \"Notes: Multiple values should be separated by semicolons. Leave it blank to use the first system contact.\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/ssl-certificate/developer-reference/api-cas-2020-04-07-listcontact\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/ssl-certificate/developer-reference/api-cas-2020-04-07-listcontact</a>\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.multiple_input_modal.title\": \"Change Alibaba Cloud contact IDs\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.multiple_input_modal.placeholder\": \"Please enter Alibaba Cloud contact ID\",\r\n  \"workflow_node.deploy.form.aliyun_clb_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.deploy.form.aliyun_clb_region.placeholder\": \"Please enter Alibaba Cloud CLB region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.deploy.form.aliyun_clb_region.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/slb/classic-load-balancer/product-overview/regions-that-support-clb\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/slb/classic-load-balancer/product-overview/regions-that-support-clb</a>\",\r\n  \"workflow_node.deploy.form.aliyun_clb_resource_type.option.loadbalancer.label\": \"CLB load balancer\",\r\n  \"workflow_node.deploy.form.aliyun_clb_resource_type.option.listener.label\": \"CLB listener\",\r\n  \"workflow_node.deploy.form.aliyun_clb_loadbalancer_id.label\": \"Alibaba Cloud CLB load balancer ID\",\r\n  \"workflow_node.deploy.form.aliyun_clb_loadbalancer_id.placeholder\": \"Please enter Alibaba Cloud CLB load balancer ID\",\r\n  \"workflow_node.deploy.form.aliyun_clb_loadbalancer_id.tooltip\": \"For more information, see <a href=\\\"https://slb.console.aliyun.com/clb\\\" target=\\\"_blank\\\">https://slb.console.aliyun.com/clb</a>\",\r\n  \"workflow_node.deploy.form.aliyun_clb_listener_port.label\": \"Alibaba Cloud CLB listener port\",\r\n  \"workflow_node.deploy.form.aliyun_clb_listener_port.placeholder\": \"Please enter Alibaba Cloud CLB listener port\",\r\n  \"workflow_node.deploy.form.aliyun_clb_listener_port.tooltip\": \"For more information, see <a href=\\\"https://slb.console.aliyun.com/clb\\\" target=\\\"_blank\\\">https://slb.console.aliyun.com/clb</a>\",\r\n  \"workflow_node.deploy.form.aliyun_clb_snidomain.label\": \"Alibaba Cloud CLB SNI domain (Optional)\",\r\n  \"workflow_node.deploy.form.aliyun_clb_snidomain.placeholder\": \"Please enter Alibaba Cloud CLB SNI domain name\",\r\n  \"workflow_node.deploy.form.aliyun_clb_snidomain.help\": \"Notes: Leave it blank to set the default certificate; otherwise, to set the extension one for SNI.\",\r\n  \"workflow_node.deploy.form.aliyun_cdn_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.deploy.form.aliyun_cdn_region.placeholder\": \"Please enter Alibaba Cloud CDN region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.deploy.form.aliyun_cdn_region.tooltip\": \"<ul style=\\\"list-style: disc;\\\"><li><strong>ap-southeast-1</strong> for Alibaba Cloud International</li><li><strong>cn-hangzhou</strong> for Alibaba Cloud in China</li></ul>\",\r\n  \"workflow_node.deploy.form.aliyun_cdn_domain.label\": \"Alibaba Cloud CDN domain\",\r\n  \"workflow_node.deploy.form.aliyun_cdn_domain.placeholder\": \"Please enter Alibaba Cloud CDN domain name\",\r\n  \"workflow_node.deploy.form.aliyun_dcdn_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.deploy.form.aliyun_dcdn_region.placeholder\": \"Please enter Alibaba Cloud DCDN region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.deploy.form.aliyun_dcdn_region.tooltip\": \"<ul style=\\\"list-style: disc;\\\"><li><strong>ap-southeast-1</strong> for Alibaba Cloud International</li><li><strong>cn-hangzhou</strong> for Alibaba Cloud in China</li></ul>\",\r\n  \"workflow_node.deploy.form.aliyun_dcdn_domain.label\": \"Alibaba Cloud DCDN domain\",\r\n  \"workflow_node.deploy.form.aliyun_dcdn_domain.placeholder\": \"Please enter Alibaba Cloud DCDN domain name\",\r\n  \"workflow_node.deploy.form.aliyun_ddospro_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.deploy.form.aliyun_ddospro_region.placeholder\": \"Please enter Alibaba Cloud Anti-DDoS region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.deploy.form.aliyun_ddospro_region.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/anti-ddos/anti-ddos-pro-and-premium/developer-reference/api-ddoscoo-2020-01-01-endpoint\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/anti-ddos/anti-ddos-pro-and-premium/developer-reference/api-ddoscoo-2020-01-01-endpoint</a>\",\r\n  \"workflow_node.deploy.form.aliyun_ddospro_domain.label\": \"Alibaba Cloud Anti-DDoS domain\",\r\n  \"workflow_node.deploy.form.aliyun_ddospro_domain.placeholder\": \"Please enter Alibaba Cloud Anti-DDoS domain name\",\r\n  \"workflow_node.deploy.form.aliyun_esa_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.deploy.form.aliyun_esa_region.placeholder\": \"Please enter Alibaba Cloud ESA region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.deploy.form.aliyun_esa_region.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint</a>\",\r\n  \"workflow_node.deploy.form.aliyun_esa_site_id.label\": \"Alibaba Cloud ESA site ID\",\r\n  \"workflow_node.deploy.form.aliyun_esa_site_id.placeholder\": \"Please enter Alibaba Cloud ESA site ID\",\r\n  \"workflow_node.deploy.form.aliyun_esa_site_id.tooltip\": \"For more information, see <a href=\\\"https://esa.console.aliyun.com/siteManage/list\\\" target=\\\"_blank\\\">https://esa.console.aliyun.com/siteManage/list</a>\",\r\n  \"workflow_node.deploy.form.aliyun_esa_saas_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.deploy.form.aliyun_esa_saas_region.placeholder\": \"Please enter Alibaba Cloud ESA region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.deploy.form.aliyun_esa_saas_region.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint</a>\",\r\n  \"workflow_node.deploy.form.aliyun_esa_saas_site_id.label\": \"Alibaba Cloud ESA site ID\",\r\n  \"workflow_node.deploy.form.aliyun_esa_saas_site_id.placeholder\": \"Please enter Alibaba Cloud ESA site ID\",\r\n  \"workflow_node.deploy.form.aliyun_esa_saas_site_id.tooltip\": \"For more information, see <a href=\\\"https://esa.console.aliyun.com/siteManage/list\\\" target=\\\"_blank\\\">https://esa.console.aliyun.com/siteManage/list</a>\",\r\n  \"workflow_node.deploy.form.aliyun_esa_saas_domain.label\": \"Alibaba Cloud ESA SaaS domain\",\r\n  \"workflow_node.deploy.form.aliyun_esa_saas_domain.placeholder\": \"Please enter Alibaba Cloud ESA SaaS domain name\",\r\n  \"workflow_node.deploy.form.aliyun_fc_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.deploy.form.aliyun_fc_region.placeholder\": \"Please enter Alibaba Cloud FC region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.deploy.form.aliyun_fc_region.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/functioncompute/fc-3-0/product-overview/supported-regions\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/functioncompute/fc-3-0/product-overview/supported-regions</a>\",\r\n  \"workflow_node.deploy.form.aliyun_fc_service_version.label\": \"Alibaba Cloud FC version\",\r\n  \"workflow_node.deploy.form.aliyun_fc_service_version.placeholder\": \"Please select Alibaba Cloud FC version\",\r\n  \"workflow_node.deploy.form.aliyun_fc_domain.label\": \"Alibaba Cloud FC domain\",\r\n  \"workflow_node.deploy.form.aliyun_fc_domain.placeholder\": \"Please enter Alibaba Cloud FC domain name\",\r\n  \"workflow_node.deploy.form.aliyun_ga_resource_type.option.accelerator.label\": \"GA accelerator\",\r\n  \"workflow_node.deploy.form.aliyun_ga_resource_type.option.listener.label\": \"GA listener\",\r\n  \"workflow_node.deploy.form.aliyun_ga_accelerator_id.label\": \"Alibaba Cloud GA accelerator ID\",\r\n  \"workflow_node.deploy.form.aliyun_ga_accelerator_id.placeholder\": \"Please enter Alibaba Cloud GA accelerator ID\",\r\n  \"workflow_node.deploy.form.aliyun_ga_accelerator_id.tooltip\": \"For more information, <a href=\\\"https://ga.console.aliyun.com\\\" target=\\\"_blank\\\">https://ga.console.aliyun.com</a>\",\r\n  \"workflow_node.deploy.form.aliyun_ga_listener_id.label\": \"Alibaba Cloud GA listener ID\",\r\n  \"workflow_node.deploy.form.aliyun_ga_listener_id.placeholder\": \"Please enter Alibaba Cloud GA listener ID\",\r\n  \"workflow_node.deploy.form.aliyun_ga_listener_id.tooltip\": \"For more information, <a href=\\\"https://ga.console.aliyun.com\\\" target=\\\"_blank\\\">https://ga.console.aliyun.com</a>\",\r\n  \"workflow_node.deploy.form.aliyun_ga_snidomain.label\": \"Alibaba Cloud GA SNI domain (Optional)\",\r\n  \"workflow_node.deploy.form.aliyun_ga_snidomain.placeholder\": \"Please enter Alibaba Cloud GA SNI domain name\",\r\n  \"workflow_node.deploy.form.aliyun_ga_snidomain.help\": \"Notes: Leave it blank to set the default certificate; otherwise, to set the extension one for SNI.\",\r\n  \"workflow_node.deploy.form.aliyun_live_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.deploy.form.aliyun_live_region.placeholder\": \"Please enter Alibaba Cloud Live region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.deploy.form.aliyun_live_region.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/live/product-overview/supported-regions\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/live/product-overview/supported-regions</a>\",\r\n  \"workflow_node.deploy.form.aliyun_live_domain.label\": \"Alibaba Cloud live streaming domain\",\r\n  \"workflow_node.deploy.form.aliyun_live_domain.placeholder\": \"Please enter Alibaba Cloud live streaming domain name\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_region.placeholder\": \"Please enter Alibaba Cloud NLB region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_region.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/slb/network-load-balancer/product-overview/regions-that-support-nlb\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/slb/network-load-balancer/product-overview/regions-that-support-nlb</a>\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_resource_type.option.loadbalancer.label\": \"NLB load balancer\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_resource_type.option.listener.label\": \"NLB listener\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_loadbalancer_id.label\": \"Alibaba Cloud NLB load balancer ID\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_loadbalancer_id.placeholder\": \"Please enter Alibaba Cloud NLB load balancer ID\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_loadbalancer_id.tooltip\": \"For more information, see <a href=\\\"https://slb.console.aliyun.com/nlb\\\" target=\\\"_blank\\\">https://slb.console.aliyun.com/nlb</a>\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_listener_id.label\": \"Alibaba Cloud NLB listener ID\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_listener_id.placeholder\": \"Please enter Alibaba Cloud NLB listener ID\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_listener_id.tooltip\": \"For more information, see <a href=\\\"https://slb.console.aliyun.com/nlb\\\" target=\\\"_blank\\\">https://slb.console.aliyun.com/nlb</a>\",\r\n  \"workflow_node.deploy.form.aliyun_oss_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.deploy.form.aliyun_oss_region.placeholder\": \"Please enter Alibaba Cloud OSS region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.deploy.form.aliyun_oss_region.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/oss/user-guide/regions-and-endpoints\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/oss/user-guide/regions-and-endpoints</a>\",\r\n  \"workflow_node.deploy.form.aliyun_oss_bucket.label\": \"Alibaba Cloud OSS bucket\",\r\n  \"workflow_node.deploy.form.aliyun_oss_bucket.placeholder\": \"Please enter Alibaba Cloud OSS bucket name\",\r\n  \"workflow_node.deploy.form.aliyun_oss_domain.label\": \"Alibaba Cloud OSS custom domain\",\r\n  \"workflow_node.deploy.form.aliyun_oss_domain.placeholder\": \"Please enter Alibaba Cloud OSS bucket custom domain name\",\r\n  \"workflow_node.deploy.form.aliyun_vod_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.deploy.form.aliyun_vod_region.placeholder\": \"Please enter Alibaba Cloud VOD region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.deploy.form.aliyun_vod_region.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/vod/product-overview/regions\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/vod/product-overview/regions</a>\",\r\n  \"workflow_node.deploy.form.aliyun_vod_domain.label\": \"Alibaba Cloud VOD domain\",\r\n  \"workflow_node.deploy.form.aliyun_vod_domain.placeholder\": \"Please enter Alibaba Cloud VOD domain name\",\r\n  \"workflow_node.deploy.form.aliyun_waf_region.label\": \"Alibaba Cloud region\",\r\n  \"workflow_node.deploy.form.aliyun_waf_region.placeholder\": \"Please enter Alibaba Cloud WAF region (e.g. cn-hangzhou)\",\r\n  \"workflow_node.deploy.form.aliyun_waf_region.tooltip\": \"For more information, see <a href=\\\"https://www.alibabacloud.com/help/en/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-endpoint\\\" target=\\\"_blank\\\">https://www.alibabacloud.com/help/en/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-endpoint</a>\",\r\n  \"workflow_node.deploy.form.aliyun_waf_service_version.label\": \"Alibaba Cloud WAF version\",\r\n  \"workflow_node.deploy.form.aliyun_waf_service_version.placeholder\": \"Please select Alibaba Cloud WAF version\",\r\n  \"workflow_node.deploy.form.aliyun_waf_service_type.label\": \"Alibaba Cloud WAF access type\",\r\n  \"workflow_node.deploy.form.aliyun_waf_service_type.placeholder\": \"Please select Alibaba Cloud WAF access type\",\r\n  \"workflow_node.deploy.form.aliyun_waf_service_type.option.cloudresource.label\": \"Cloud product access\",\r\n  \"workflow_node.deploy.form.aliyun_waf_service_type.option.cname.label\": \"CNAME access\",\r\n  \"workflow_node.deploy.form.aliyun_waf_instance_id.label\": \"Alibaba Cloud WAF instance ID\",\r\n  \"workflow_node.deploy.form.aliyun_waf_instance_id.placeholder\": \"Please enter Alibaba Cloud WAF instance ID\",\r\n  \"workflow_node.deploy.form.aliyun_waf_instance_id.tooltip\": \"For more information, see <a href=\\\"https://waf.console.aliyun.com\\\" target=\\\"_blank\\\">https://waf.console.aliyun.com</a>\",\r\n  \"workflow_node.deploy.form.aliyun_waf_resource_product.label\": \"Alibaba Cloud WAF accessed resource product\",\r\n  \"workflow_node.deploy.form.aliyun_waf_resource_product.placeholder\": \"Please enter Alibaba Cloud WAF accessed resource product\",\r\n  \"workflow_node.deploy.form.aliyun_waf_resource_id.label\": \"Alibaba Cloud WAF accessed resource ID\",\r\n  \"workflow_node.deploy.form.aliyun_waf_resource_id.placeholder\": \"Please enter Alibaba Cloud WAF accessed resource ID\",\r\n  \"workflow_node.deploy.form.aliyun_waf_resource_port.label\": \"Alibaba Cloud WAF accessed resource port\",\r\n  \"workflow_node.deploy.form.aliyun_waf_resource_port.placeholder\": \"Please enter Alibaba Cloud WAF accessed resource port\",\r\n  \"workflow_node.deploy.form.aliyun_waf_domain.label\": \"Alibaba Cloud WAF SNI domain (Optional)\",\r\n  \"workflow_node.deploy.form.aliyun_waf_domain.placeholder\": \"Please enter Alibaba Cloud WAF SNI domain name\",\r\n  \"workflow_node.deploy.form.aliyun_waf_domain.help\": \"Notes: Leave it blank to set the default certificate; otherwise, to set the extension one for SNI.\",\r\n  \"workflow_node.deploy.form.apisix.guide\": \"Requires APISIX v2.0 or higher.\",\r\n  \"workflow_node.deploy.form.apisix_resource_type.option.certificate.label\": \"SSL certificate\",\r\n  \"workflow_node.deploy.form.apisix_certificate_id.label\": \"APISIX certificate ID\",\r\n  \"workflow_node.deploy.form.apisix_certificate_id.placeholder\": \"Please enter APISIX certificate ID\",\r\n  \"workflow_node.deploy.form.apisix_certificate_id.tooltip\": \"You can find it on APISIX dashboard.\",\r\n  \"workflow_node.deploy.form.aws_acm_region.label\": \"AWS Region\",\r\n  \"workflow_node.deploy.form.aws_acm_region.placeholder\": \"Please enter AWS ACM region (e.g. us-east-1)\",\r\n  \"workflow_node.deploy.form.aws_acm_region.tooltip\": \"For more information, see <a href=\\\"https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints</a>\",\r\n  \"workflow_node.deploy.form.aws_acm_certificate_arn.label\": \"AWS ACM certificate ARN (Optional)\",\r\n  \"workflow_node.deploy.form.aws_acm_certificate_arn.placeholder\": \"Please enter AWS ACM certificate ARN\",\r\n  \"workflow_node.deploy.form.aws_acm_certificate_arn.help\": \"Notes: Leave it blank to import a new certificate.\",\r\n  \"workflow_node.deploy.form.aws_cloudfront_region.label\": \"AWS Region\",\r\n  \"workflow_node.deploy.form.aws_cloudfront_region.placeholder\": \"Please enter AWS CloudFront region (e.g. us-east-1)\",\r\n  \"workflow_node.deploy.form.aws_cloudfront_region.tooltip\": \"For more information, see <a href=\\\"https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints</a>\",\r\n  \"workflow_node.deploy.form.aws_cloudfront_distribution_id.label\": \"AWS CloudFront distribution ID\",\r\n  \"workflow_node.deploy.form.aws_cloudfront_distribution_id.placeholder\": \"Please enter AWS CloudFront distribution ID\",\r\n  \"workflow_node.deploy.form.aws_cloudfront_distribution_id.tooltip\": \"For more information, see <a href=\\\"https://docs.aws.amazon.com/en_us/AmazonCloudFront/latest/DeveloperGuide/distribution-working-with.html\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/en_us/AmazonCloudFront/latest/DeveloperGuide/distribution-working-with.html</a>\",\r\n  \"workflow_node.deploy.form.aws_cloudfront_certificate_source.label\": \"AWS CloudFront certificate source\",\r\n  \"workflow_node.deploy.form.aws_cloudfront_certificate_source.placeholder\": \"Please select AWS CloudFront certificate source\",\r\n  \"workflow_node.deploy.form.aws_iam_region.label\": \"AWS Region\",\r\n  \"workflow_node.deploy.form.aws_iam_region.placeholder\": \"Please enter AWS IAM region (e.g. us-east-1)\",\r\n  \"workflow_node.deploy.form.aws_iam_region.tooltip\": \"For more information, see <a href=\\\"https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints</a>\",\r\n  \"workflow_node.deploy.form.aws_iam_certificate_path.label\": \"AWS IAM certificate path (Optional)\",\r\n  \"workflow_node.deploy.form.aws_iam_certificate_path.placeholder\": \"Please enter AWS IAM certificate path\",\r\n  \"workflow_node.deploy.form.aws_iam_certificate_path.errmsg.invalid\": \"Please enter a valid AWS IAM certificate path\",\r\n  \"workflow_node.deploy.form.aws_iam_certificate_path.tooltip\": \"For more information, see <a href=\\\"https://docs.aws.amazon.com/en_us/IAM/latest/UserGuide/reference_identifiers.html\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/en_us/IAM/latest/UserGuide/reference_identifiers.html</a>\",\r\n  \"workflow_node.deploy.form.azure_keyvault_name.label\": \"Azure KeyVault name\",\r\n  \"workflow_node.deploy.form.azure_keyvault_name.placeholder\": \"Please enter Azure KeyVault name\",\r\n  \"workflow_node.deploy.form.azure_keyvault_name.tooltip\": \"For more information, see <a href=\\\"https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates\\\" target=\\\"_blank\\\">https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates</a>\",\r\n  \"workflow_node.deploy.form.azure_keyvault_certificate_name.label\": \"Azure KeyVault certificate name (Optional)\",\r\n  \"workflow_node.deploy.form.azure_keyvault_certificate_name.placeholder\": \"Please enter Azure KeyVault certificate name\",\r\n  \"workflow_node.deploy.form.azure_keyvault_certificate_name.errmsg.invalid\": \"Certificate name can only contain letters, numbers, and hyphens (-), with a length limit of 1 to 127 characters\",\r\n  \"workflow_node.deploy.form.azure_keyvault_certificate_name.help\": \"Notes: Leave it blank to use a default name generated by Certimate.\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_region.label\": \"Baidu Cloud region\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_region.placeholder\": \"Please enter Baidu Cloud BLB region (e.g. bj)\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_region.tooltip\": \"For more information, see <a href=\\\"https://cloud.baidu.com/doc/BLB/s/cjwvxnzix\\\" target=\\\"_blank\\\">https://cloud.baidu.com/doc/BLB/s/cjwvxnzix</a>\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_resource_type.option.loadbalancer.label\": \"BLB load balancer\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_resource_type.option.listener.label\": \"BLB listener\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.label\": \"Baidu Cloud BLB load balancer ID\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder\": \"Please enter Baidu Cloud BLB load balancer ID\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.tooltip\": \"For more information, see <a href=\\\"https://console.bce.baidu.com/blb/#/appblb/list\\\" target=\\\"_blank\\\">https://console.bce.baidu.com/blb/#/appblb/list</a>\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_listener_port.label\": \"Baidu Cloud BLB listener port\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_listener_port.placeholder\": \"Please enter Baidu Cloud BLB listener port\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_listener_port.tooltip\": \"For more information, see <a href=\\\"https://console.bce.baidu.com/blb/#/appblb/list\\\" target=\\\"_blank\\\">https://console.bce.baidu.com/blb/#/appblb/list</a>\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_snidomain.label\": \"Baidu Cloud BLB SNI domain (Optional)\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_snidomain.placeholder\": \"Please enter Baidu Cloud BLB SNI domain name\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_snidomain.help\": \"Notes: Leave it blank to set the default certificate; otherwise, to set the extension one for SNI.\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_region.label\": \"Baidu Cloud region\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_region.placeholder\": \"Please enter Baidu Cloud BLB region (e.g. bj)\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_region.tooltip\": \"For more information, see <a href=\\\"https://cloud.baidu.com/doc/BLB/s/cjwvxnzix\\\" target=\\\"_blank\\\">https://cloud.baidu.com/doc/BLB/s/cjwvxnzix</a>\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_resource_type.option.loadbalancer.label\": \"BLB load balancer\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_resource_type.option.listener.label\": \"BLB listener\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.label\": \"Baidu Cloud BLB load balancer ID\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder\": \"Please enter Baidu Cloud BLB load balancer ID\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.tooltip\": \"For more information, see <a href=\\\"https://console.bce.baidu.com/blb/#/blb/list\\\" target=\\\"_blank\\\">https://console.bce.baidu.com/blb/#/blb/list</a>\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_listener_port.label\": \"Baidu Cloud BLB listener port\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_listener_port.placeholder\": \"Please enter Baidu Cloud BLB listener port\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_listener_port.tooltip\": \"For more information, see <a href=\\\"https://console.bce.baidu.com/blb/#/blb/list\\\" target=\\\"_blank\\\">https://console.bce.baidu.com/blb/#/blb/list</a>\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_snidomain.label\": \"Baidu Cloud BLB SNI domain (Optional)\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_snidomain.placeholder\": \"Please enter Baidu Cloud BLB SNI domain name\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_snidomain.help\": \"Notes: Leave it blank to set the default certificate; otherwise, to set the extension one for SNI.\",\r\n  \"workflow_node.deploy.form.baiducloud_cdn_domain.label\": \"Baidu Cloud CDN domain\",\r\n  \"workflow_node.deploy.form.baiducloud_cdn_domain.placeholder\": \"Please enter Baidu Cloud CDN domain name\",\r\n  \"workflow_node.deploy.form.baishan_cdn_resource_type.option.domain.label\": \"Domain\",\r\n  \"workflow_node.deploy.form.baishan_cdn_resource_type.option.certificate.label\": \"Certificate\",\r\n  \"workflow_node.deploy.form.baishan_cdn_domain.label\": \"Baishan Cloud CDN domain\",\r\n  \"workflow_node.deploy.form.baishan_cdn_domain.placeholder\": \"Please enter Baishan Cloud CDN domain name\",\r\n  \"workflow_node.deploy.form.baishan_cdn_certificate_id.label\": \"Baishan Cloud CDN certificate ID\",\r\n  \"workflow_node.deploy.form.baishan_cdn_certificate_id.placeholder\": \"Please enter Baishan Cloud CDN certificate ID\",\r\n  \"workflow_node.deploy.form.baishan_cdn_certificate_id.tooltip\": \"For more information, see <a href=\\\"https://cdnx.console.baishan.com/#/cdn/cert\\\" target=\\\"_blank\\\">https://cdnx.console.baishan.com/#/cdn/cert</a>\",\r\n  \"workflow_node.deploy.form.baotapanel.guide\": \"Requires aaPanel v7.0 or higher.\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.label\": \"aaPanel website type\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.placeholder\": \"Please select aaPanel website type\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.php.label\": \"PHP Project\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.java.label\": \"Java Project\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.nodejs.label\": \"Node.js Project\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.go.label\": \"Golang Project\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.python.label\": \"Python Project\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.proxy.label\": \"Reverse Proxy\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.html.label\": \"HTML Project\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.general.label\": \"General Project\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.any.label\": \"Any Project (aaPanel v7.26+)\",\r\n  \"workflow_node.deploy.form.baotapanel_site_names.label\": \"aaPanel website names\",\r\n  \"workflow_node.deploy.form.baotapanel_site_names.placeholder\": \"Please enter aaPanel website names (separated by semicolons)\",\r\n  \"workflow_node.deploy.form.baotapanel_site_names.errmsg.invalid\": \"Please enter a valid aaPanel website name\",\r\n  \"workflow_node.deploy.form.baotapanel_site_names.help\": \"Notes: Multiple values should be separated by semicolons.\",\r\n  \"workflow_node.deploy.form.baotapanel_site_names.tooltip\": \"You can find it on aaPanel dashboard.\",\r\n  \"workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.title\": \"Change aaPanel website names\",\r\n  \"workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.placeholder\": \"Please enter aaPanel website name\",\r\n  \"workflow_node.deploy.form.baotapanel_console.guide\": \"Requires aaPanel v7.0 or higher.\",\r\n  \"workflow_node.deploy.form.baotapanel_console_auto_restart.label\": \"Auto restart aaPanel after deployment\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.label\": \"aaPanel WinGo website type\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.placeholder\": \"Please select aaPanel WinGo website type\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.option.php.label\": \"PHP Project\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.option.java.label\": \"Java Project\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.option.asp.label\": \".NET Project\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.option.go.label\": \"Golang Project\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.option.python.label\": \"Python Project\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.option.nodejs.label\": \"Node.js Project\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.option.proxy.label\": \"Reverse Proxy\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.option.general.label\": \"General Project\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_names.label\": \"aaPanel WinGo website names\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_names.placeholder\": \"Please enter aaPanel WinGo website names (separated by semicolons)\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_names.errmsg.invalid\": \"Please enter a valid aaPanel WinGo website name\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_names.help\": \"Notes: Multiple values should be separated by semicolons.\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_names.tooltip\": \"You can find it on aaPanel WinGo dashboard.\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_names.multiple_input_modal.title\": \"Change aaPanel WinGo website names\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_names.multiple_input_modal.placeholder\": \"Please enter aaPanel WinGo website name\",\r\n  \"workflow_node.deploy.form.baotawaf.guide\": \"Requires aaWAF v5.7 or higher.\",\r\n  \"workflow_node.deploy.form.baotawaf_site_names.label\": \"aaWAF website names\",\r\n  \"workflow_node.deploy.form.baotawaf_site_names.placeholder\": \"Please enter aaWAF website names (separated by semicolons)\",\r\n  \"workflow_node.deploy.form.baotawaf_site_names.errmsg.invalid\": \"Please enter a valid aaWAF website name\",\r\n  \"workflow_node.deploy.form.baotawaf_site_names.help\": \"Notes: Multiple values should be separated by semicolons.\",\r\n  \"workflow_node.deploy.form.baotawaf_site_names.tooltip\": \"You can find it on aaWAF dashboard.\",\r\n  \"workflow_node.deploy.form.baotawaf_site_names.multiple_input_modal.title\": \"Change aaWAF website names\",\r\n  \"workflow_node.deploy.form.baotawaf_site_names.multiple_input_modal.placeholder\": \"Please enter aaWAF website name\",\r\n  \"workflow_node.deploy.form.baotawaf_site_port.label\": \"aaWAF website SSL port\",\r\n  \"workflow_node.deploy.form.baotawaf_site_port.placeholder\": \"Please enter aaWAF SSL port\",\r\n  \"workflow_node.deploy.form.baotawaf_console.guide\": \"Requires aaWAF v5.7 or higher.\",\r\n  \"workflow_node.deploy.form.bunny_cdn_pull_zone_id.label\": \"Bunny CDN pull zone ID\",\r\n  \"workflow_node.deploy.form.bunny_cdn_pull_zone_id.placeholder\": \"Please enter Bunny CDN pull zone ID\",\r\n  \"workflow_node.deploy.form.bunny_cdn_pull_zone_id.tooltip\": \"What is this? See <a href=\\\"https://dash.bunny.net/cdn\\\" target=\\\"_blank\\\">https://dash.bunny.net/cdn</a>\",\r\n  \"workflow_node.deploy.form.bunny_cdn_hostname.label\": \"Bunny CDN hostname\",\r\n  \"workflow_node.deploy.form.bunny_cdn_hostname.placeholder\": \"Please enter Bunny CDN hostname\",\r\n  \"workflow_node.deploy.form.bunny_cdn_hostname.tooltip\": \"What is this? See <a href=\\\"https://dash.bunny.net/cdn\\\" target=\\\"_blank\\\">https://dash.bunny.net/cdn</a>\",\r\n  \"workflow_node.deploy.form.byteplus_cdn_domain.label\": \"BytePlus CDN domain\",\r\n  \"workflow_node.deploy.form.byteplus_cdn_domain.placeholder\": \"Please enter BytePlus CDN domain name\",\r\n  \"workflow_node.deploy.form.cdnfly_resource_type.option.website.label\": \"Website\",\r\n  \"workflow_node.deploy.form.cdnfly_resource_type.option.certificate.label\": \"Certificate\",\r\n  \"workflow_node.deploy.form.cdnfly_site_id.label\": \"Cdnfly website ID\",\r\n  \"workflow_node.deploy.form.cdnfly_site_id.placeholder\": \"Please enter Cdnfly website ID\",\r\n  \"workflow_node.deploy.form.cdnfly_site_id.tooltip\": \"You can find it on Cdnfly dashboard.\",\r\n  \"workflow_node.deploy.form.cdnfly_certificate_id.label\": \"Cdnfly certificate ID\",\r\n  \"workflow_node.deploy.form.cdnfly_certificate_id.placeholder\": \"Please enter Cdnfly certificate ID\",\r\n  \"workflow_node.deploy.form.cdnfly_certificate_id.tooltip\": \"You can find it on Cdnfly dashboard.\",\r\n  \"workflow_node.deploy.form.cpanel_resource_type.option.website.label\": \"Website\",\r\n  \"workflow_node.deploy.form.cpanel_domain.label\": \"cPanel website domain\",\r\n  \"workflow_node.deploy.form.cpanel_domain.placeholder\": \"Please enter cPanel website domain\",\r\n  \"workflow_node.deploy.form.ctcccloud_ao_domain.label\": \"CTCC StateCloud AccessOne domain\",\r\n  \"workflow_node.deploy.form.ctcccloud_ao_domain.placeholder\": \"Please enter CTCC StateCloud AccessOne domain name\",\r\n  \"workflow_node.deploy.form.ctcccloud_cdn_domain.label\": \"CTCC StateCloud CDN domain\",\r\n  \"workflow_node.deploy.form.ctcccloud_cdn_domain.placeholder\": \"Please enter CTCC StateCloud CDN domain name\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_region_id.label\": \"CTCC StateCloud region ID\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_region_id.placeholder\": \"Please enter CTCC StateCloud ELB region ID\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_region_id.tooltip\": \"For more information, see <a href=\\\"https://www.ctyun.cn/document/10026755/10196575\\\" target=\\\"_blank\\\">https://www.ctyun.cn/document/10026755/10196575</a>\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_resource_type.option.certificate.label\": \"ELB certificate\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_resource_type.option.loadbalancer.label\": \"ELB load balancer\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_resource_type.option.listener.label\": \"ELB listener\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.label\": \"CTCC StateCloud ELB load balancer ID\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.placeholder\": \"Please enter CTCC StateCloud ELB load balancer ID\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.tooltip\": \"For more information, see <a href=\\\"https://console.ctyun.cn/network/index/#/elb/elbList\\\" target=\\\"_blank\\\">https://console.ctyun.cn/network/index/#/elb/elbList</a>\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_listener_id.label\": \"CTCC StateCloud ELB listener ID\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_listener_id.placeholder\": \"Please enter CTCC StateCloud ELB listener ID\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_listener_id.tooltip\": \"For more information, see <a href=\\\"https://console.ctyun.cn/network/index/#/elb/elbList\\\" target=\\\"_blank\\\">https://console.ctyun.cn/network/index/#/elb/elbList</a>\",\r\n  \"workflow_node.deploy.form.ctcccloud_faas_region_id.label\": \"CTCC StateCloud region ID\",\r\n  \"workflow_node.deploy.form.ctcccloud_faas_region_id.placeholder\": \"Please enter CTCC StateCloud FaaS region ID\",\r\n  \"workflow_node.deploy.form.ctcccloud_faas_region_id.tooltip\": \"For more information, see <a href=\\\"https://www.ctyun.cn/document/10026755/10196575\\\" target=\\\"_blank\\\">https://www.ctyun.cn/document/10026755/10196575</a>\",\r\n  \"workflow_node.deploy.form.ctcccloud_faas_domain.label\": \"CTCC StateCloud FaaS domain\",\r\n  \"workflow_node.deploy.form.ctcccloud_faas_domain.placeholder\": \"Please enter CTCC StateCloud FaaS domain name\",\r\n  \"workflow_node.deploy.form.ctcccloud_icdn_domain.label\": \"CTCC StateCloud ICDN domain\",\r\n  \"workflow_node.deploy.form.ctcccloud_icdn_domain.placeholder\": \"Please enter CTCC StateCloud ICDN domain name\",\r\n  \"workflow_node.deploy.form.ctcccloud_lvdn_domain.label\": \"CTCC StateCloud LVDN domain\",\r\n  \"workflow_node.deploy.form.ctcccloud_lvdn_domain.placeholder\": \"Please enter CTCC StateCloud LVDN domain name\",\r\n  \"workflow_node.deploy.form.dogecloud_cdn_domain.label\": \"Doge Cloud CDN domain\",\r\n  \"workflow_node.deploy.form.dogecloud_cdn_domain.placeholder\": \"Please enter Doge Cloud CDN domain name\",\r\n  \"workflow_node.deploy.form.flexcdn_resource_type.option.certificate.label\": \"Certificate\",\r\n  \"workflow_node.deploy.form.flexcdn_certificate_id.label\": \"FlexCDN certificate ID\",\r\n  \"workflow_node.deploy.form.flexcdn_certificate_id.placeholder\": \"Please enter FlexCDN certificate ID\",\r\n  \"workflow_node.deploy.form.flexcdn_certificate_id.tooltip\": \"You can find it on FlexCDN dashboard.\",\r\n  \"workflow_node.deploy.form.flyio_app_name.label\": \"Fly.io App name\",\r\n  \"workflow_node.deploy.form.flyio_app_name.placeholder\": \"Please enter Fly.io App name\",\r\n  \"workflow_node.deploy.form.flyio_domain.label\": \"Fly.io custom domain\",\r\n  \"workflow_node.deploy.form.flyio_domain.placeholder\": \"Please enter Fly.io custom domain name\",\r\n  \"workflow_node.deploy.form.flyio_domain.help\": \"Notes: After importing the custom certificate for the first time, you need to complete domain ownership verification on Fly.io dashboard.\",\r\n  \"workflow_node.deploy.form.gcore_cdn_resource_id.label\": \"G-Core CDN resource ID\",\r\n  \"workflow_node.deploy.form.gcore_cdn_resource_id.placeholder\": \"Please enter G-Core CDN resource ID\",\r\n  \"workflow_node.deploy.form.gcore_cdn_resource_id.tooltip\": \"For more information, see <a href=\\\"https://cdn.gcore.com/resources/list\\\" target=\\\"_blank\\\">https://cdn.gcore.com/resources/list</a>\",\r\n  \"workflow_node.deploy.form.gcore_cdn_certificate_id.label\": \"G-Core CDN certificate ID (Optional)\",\r\n  \"workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder\": \"Please enter G-Core CDN certificate ID\",\r\n  \"workflow_node.deploy.form.gcore_cdn_certificate_id.help\": \"Notes: Leave it blank to import a new certificate; otherwise, to replace the existing one.\",\r\n  \"workflow_node.deploy.form.gcore_cdn_certificate_id.tooltip\": \"For more information, see <a href=\\\"https://cdn.gcore.com/ssl\\\" target=\\\"_blank\\\">https://cdn.gcore.com/ssl</a>\",\r\n  \"workflow_node.deploy.form.goedge_resource_type.option.certificate.label\": \"Certificate\",\r\n  \"workflow_node.deploy.form.goedge_certificate_id.label\": \"GoEdge certificate ID\",\r\n  \"workflow_node.deploy.form.goedge_certificate_id.placeholder\": \"Please enter GoEdge certificate ID\",\r\n  \"workflow_node.deploy.form.goedge_certificate_id.tooltip\": \"You can find it on GoEdge dashboard.\",\r\n  \"workflow_node.deploy.form.huaweicloud_cdn_region.label\": \"Huawei Cloud region\",\r\n  \"workflow_node.deploy.form.huaweicloud_cdn_region.placeholder\": \"Please enter Huawei Cloud CDN region (e.g. cn-north-1)\",\r\n  \"workflow_node.deploy.form.huaweicloud_cdn_region.tooltip\": \"For more information, see <a href=\\\"https://console-intl.huaweicloud.com/apiexplorer/#/endpoint?locale=en-us\\\" target=\\\"_blank\\\">https://console-intl.huaweicloud.com/apiexplorer/#/endpoint</a>\",\r\n  \"workflow_node.deploy.form.huaweicloud_cdn_domain.label\": \"Huawei Cloud CDN domain\",\r\n  \"workflow_node.deploy.form.huaweicloud_cdn_domain.placeholder\": \"Please enter Huawei Cloud CDN domain name\",\r\n  \"workflow_node.deploy.form.huaweicloud_obs_region.label\": \"Huawei Cloud region\",\r\n  \"workflow_node.deploy.form.huaweicloud_obs_region.placeholder\": \"Please enter Huawei Cloud OBS region (e.g. cn-north-1)\",\r\n  \"workflow_node.deploy.form.huaweicloud_obs_region.tooltip\": \"For more information, see <a href=\\\"https://console-intl.huaweicloud.com/apiexplorer/#/endpoint?locale=en-us\\\" target=\\\"_blank\\\">https://console-intl.huaweicloud.com/apiexplorer/#/endpoint</a>\",\r\n  \"workflow_node.deploy.form.huaweicloud_obs_bucket.label\": \"Huawei Cloud OBS bucket name\",\r\n  \"workflow_node.deploy.form.huaweicloud_obs_bucket.placeholder\": \"Please enter Huawei Cloud OBS bucket name\",\r\n  \"workflow_node.deploy.form.huaweicloud_obs_domain.label\": \"Huawei Cloud OBS custom domain\",\r\n  \"workflow_node.deploy.form.huaweicloud_obs_domain.placeholder\": \"Please enter Huawei Cloud OBS custom domain name\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_region.label\": \"Huawei Cloud region\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_region.placeholder\": \"Please enter Huawei Cloud ELB region (e.g. cn-north-1)\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_region.tooltip\": \"For more information, see <a href=\\\"https://console-intl.huaweicloud.com/apiexplorer/#/endpoint?locale=en-us\\\" target=\\\"_blank\\\">https://console-intl.huaweicloud.com/apiexplorer/#/endpoint</a>\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_resource_type.option.loadbalancer.label\": \"ELB load balancer\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_resource_type.option.listener.label\": \"ELB listener\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_resource_type.option.certificate.label\": \"ELB certificate\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_certificate_id.label\": \"Huawei Cloud ELB certificate ID\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_certificate_id.placeholder\": \"Please enter Huawei Cloud ELB certificate ID\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_certificate_id.tooltip\": \"For more information, see <a href=\\\"https://console-intl.huaweicloud.com/vpc/#/elb/elbCert\\\" target=\\\"_blank\\\">https://console-intl.huaweicloud.com/vpc/#/elb/elbCert</a>\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_loadbalancer_id.label\": \"Huawei Cloud ELB load balancer ID\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_loadbalancer_id.placeholder\": \"Please enter Huawei Cloud ELB load balancer ID\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_loadbalancer_id.tooltip\": \"For more information, see <a href=\\\"https://console-intl.huaweicloud.com/vpc/#/elb/list/grid\\\" target=\\\"_blank\\\">https://console-intl.huaweicloud.com/vpc/#/elb/list/grid</a>\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_listener_id.label\": \"Huawei Cloud ELB listener ID\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_listener_id.placeholder\": \"Please enter Huawei Cloud ELB listener ID\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_listener_id.tooltip\": \"For more information, see <a href=\\\"https://console-intl.huaweicloud.com/vpc/#/elb/list/grid\\\" target=\\\"_blank\\\">https://console-intl.huaweicloud.com/vpc/#/elb/list/grid</a>\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_region.label\": \"Huawei Cloud region\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_region.placeholder\": \"Please enter Huawei Cloud WAF region (e.g. cn-north-1)\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_region.tooltip\": \"For more information, see <a href=\\\"https://console-intl.huaweicloud.com/apiexplorer/#/endpoint?locale=en-us\\\" target=\\\"_blank\\\">https://console-intl.huaweicloud.com/apiexplorer/#/endpoint</a>\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_resource_type.option.cloudserver.label\": \"WAF cloud server\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_resource_type.option.premiumhost.label\": \"WAF premium host\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_resource_type.option.certificate.label\": \"WAF certificate\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_domain.label\": \"Huawei Cloud WAF domain\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_domain.placeholder\": \"Please enter Huawei Cloud WAF domain name\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_certificate_id.label\": \"Huawei Cloud WAF certificate ID\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_certificate_id.placeholder\": \"Please enter Huawei Cloud WAF certificate ID\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_certificate_id.tooltip\": \"For more information, see <a href=\\\"https://console-intl.huaweicloud.com/console/#/waf/certificateManagement\\\" target=\\\"_blank\\\">https://console-intl.huaweicloud.com/console/#/waf/certificateManagement</a>\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_region_id.label\": \"JD Cloud region ID\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_region_id.placeholder\": \"Please enter JD Cloud ALB region ID (e.g. cn-north-1)\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_region_id.tooltip\": \"For more information, see <a href=\\\"https://docs.jdcloud.com/en/common-declaration/api/introduction\\\" target=\\\"_blank\\\">https://docs.jdcloud.com/en/common-declaration/api/introduction</a>\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_resource_type.option.loadbalancer.label\": \"ALB load balancer\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_resource_type.option.listener.label\": \"ALB listener\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_loadbalancer_id.label\": \"JD Cloud ALB load balancer ID\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_loadbalancer_id.placeholder\": \"Please enter JD Cloud ALB load balancer ID\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_loadbalancer_id.tooltip\": \"For more information, see <a href=\\\"https://cns-console.jdcloud.com/host/loadBalance/list\\\" target=\\\"_blank\\\">https://cns-console.jdcloud.com/host/loadBalance/list</a>\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_listener_id.label\": \"JD Cloud ALB listener ID\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_listener_id.placeholder\": \"Please enter JD Cloud ALB listener ID\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_listener_id.tooltip\": \"For more information, see <a href=\\\"https://cns-console.jdcloud.com/host/loadBalance/list\\\" target=\\\"_blank\\\">https://cns-console.jdcloud.com/host/loadBalance/list</a>\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_snidomain.label\": \"JD Cloud ALB SNI domain (Optional)\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_snidomain.placeholder\": \"Please enter JD Cloud ALB SNI domain name\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_snidomain.help\": \"Notes: Leave it blank to set the default certificate; otherwise, to set the extension one for SNI.\",\r\n  \"workflow_node.deploy.form.jdcloud_cdn_domain.label\": \"JD Cloud CDN domain\",\r\n  \"workflow_node.deploy.form.jdcloud_cdn_domain.placeholder\": \"Please enter JD Cloud CDN domain name\",\r\n  \"workflow_node.deploy.form.jdcloud_live_domain.label\": \"JD Cloud Live Video play domain\",\r\n  \"workflow_node.deploy.form.jdcloud_live_domain.placeholder\": \"Please enter JD Cloud Live Video play domain name\",\r\n  \"workflow_node.deploy.form.jdcloud_vod_domain.label\": \"JD Cloud VOD domain\",\r\n  \"workflow_node.deploy.form.jdcloud_vod_domain.placeholder\": \"Please enter JD Cloud VOD domain name\",\r\n  \"workflow_node.deploy.form.k8s_namespace.label\": \"Kubernetes Namespace\",\r\n  \"workflow_node.deploy.form.k8s_namespace.placeholder\": \"Please enter Kubernetes Namespace\",\r\n  \"workflow_node.deploy.form.k8s_namespace.tooltip\": \"For more information, see <a href=\\\"https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/\\\" target=\\\"_blank\\\">https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/</a>\",\r\n  \"workflow_node.deploy.form.k8s_secret_name.label\": \"Kubernetes Secret name\",\r\n  \"workflow_node.deploy.form.k8s_secret_name.placeholder\": \"Please enter Kubernetes Secret name\",\r\n  \"workflow_node.deploy.form.k8s_secret_name.tooltip\": \"For more information, see <a href=\\\"https://kubernetes.io/docs/concepts/configuration/secret/\\\" target=\\\"_blank\\\">https://kubernetes.io/docs/concepts/configuration/secret/</a>\",\r\n  \"workflow_node.deploy.form.k8s_secret_type.label\": \"Kubernetes Secret type\",\r\n  \"workflow_node.deploy.form.k8s_secret_type.placeholder\": \"Please enter Kubernetes Secret type\",\r\n  \"workflow_node.deploy.form.k8s_secret_type.tooltip\": \"For more information, see <a href=\\\"https://kubernetes.io/docs/concepts/configuration/secret/\\\" target=\\\"_blank\\\">https://kubernetes.io/docs/concepts/configuration/secret/</a>\",\r\n  \"workflow_node.deploy.form.k8s_secret_data_key_for_crt.label\": \"Kubernetes Secret data key for certificate\",\r\n  \"workflow_node.deploy.form.k8s_secret_data_key_for_crt.placeholder\": \"Please enter Kubernetes Secret data key for certificate\",\r\n  \"workflow_node.deploy.form.k8s_secret_data_key_for_crt.tooltip\": \"For more information, see <a href=\\\"https://kubernetes.io/docs/concepts/configuration/secret/\\\" target=\\\"_blank\\\">https://kubernetes.io/docs/concepts/configuration/secret/</a>\",\r\n  \"workflow_node.deploy.form.k8s_secret_data_key_for_key.label\": \"Kubernetes Secret data key for private key\",\r\n  \"workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder\": \"Please enter Kubernetes Secret data key for private key\",\r\n  \"workflow_node.deploy.form.k8s_secret_data_key_for_key.tooltip\": \"For more information, see <a href=\\\"https://kubernetes.io/docs/concepts/configuration/secret/\\\" target=\\\"_blank\\\">https://kubernetes.io/docs/concepts/configuration/secret/</a>\",\r\n  \"workflow_node.deploy.form.k8s_secret_annotations.label\": \"Kubernetes Secret annotations (Optional)\",\r\n  \"workflow_node.deploy.form.k8s_secret_annotations.placeholder\": \"Please enter Kubernetes Secret annotations\",\r\n  \"workflow_node.deploy.form.k8s_secret_annotations.help\": \"Notes: One key value pair per line, separated by colon.\",\r\n  \"workflow_node.deploy.form.k8s_secret_annotations.errmsg.invalid\": \"Please enter a valid annotations\",\r\n  \"workflow_node.deploy.form.k8s_secret_annotations.tooltip\": \"Example: <br><i>environment: production<br>app: nginx</i>\",\r\n  \"workflow_node.deploy.form.k8s_secret_labels.label\": \"Kubernetes Secret labels (Optional)\",\r\n  \"workflow_node.deploy.form.k8s_secret_labels.placeholder\": \"Please enter Kubernetes Secret labels\",\r\n  \"workflow_node.deploy.form.k8s_secret_labels.help\": \"Notes: One key value pair per line, separated by colon.\",\r\n  \"workflow_node.deploy.form.k8s_secret_labels.errmsg.invalid\": \"Please enter a valid labels\",\r\n  \"workflow_node.deploy.form.k8s_secret_labels.tooltip\": \"Example: <br><i>environment: production<br>app: nginx</i>\",\r\n  \"workflow_node.deploy.form.kong.guide\": \"Requires Kong v2.0 or higher.\",\r\n  \"workflow_node.deploy.form.kong_resource_type.option.certificate.label\": \"SSL certificate\",\r\n  \"workflow_node.deploy.form.kong_workspace.label\": \"Kong workspace (Optional)\",\r\n  \"workflow_node.deploy.form.kong_workspace.placeholder\": \"Please enter Kong workspace\",\r\n  \"workflow_node.deploy.form.kong_workspace.tooltip\": \"You can find it on Kong dashboard.\",\r\n  \"workflow_node.deploy.form.kong_certificate_id.label\": \"Kong certificate ID\",\r\n  \"workflow_node.deploy.form.kong_certificate_id.placeholder\": \"Please enter Kong certificate ID\",\r\n  \"workflow_node.deploy.form.kong_certificate_id.tooltip\": \"You can find it on Kong dashboard.\",\r\n  \"workflow_node.deploy.form.ksyun_cdn_resource_type.option.domain.label\": \"Domain\",\r\n  \"workflow_node.deploy.form.ksyun_cdn_resource_type.option.certificate.label\": \"Certificate\",\r\n  \"workflow_node.deploy.form.ksyun_cdn_domain.label\": \"Kingsoft Cloud CDN domain\",\r\n  \"workflow_node.deploy.form.ksyun_cdn_domain.placeholder\": \"Please enter Kingsoft Cloud CDN domain name\",\r\n  \"workflow_node.deploy.form.ksyun_cdn_certificate_id.label\": \"Kingsoft Cloud CDN certificate ID\",\r\n  \"workflow_node.deploy.form.ksyun_cdn_certificate_id.placeholder\": \"Please enter Kingsoft Cloud CDN certificate ID\",\r\n  \"workflow_node.deploy.form.ksyun_cdn_certificate_id.tooltip\": \"For more information, see <a href=\\\"https://cdn.console.ksyun.com/\\\" target=\\\"_blank\\\">https://cdn.console.ksyun.com/</a>\",\r\n  \"workflow_node.deploy.form.lecdn_resource_type.option.certificate.label\": \"Certificate\",\r\n  \"workflow_node.deploy.form.lecdn_certificate_id.label\": \"LeCDN certificate ID\",\r\n  \"workflow_node.deploy.form.lecdn_certificate_id.placeholder\": \"Please enter LeCDN certificate ID\",\r\n  \"workflow_node.deploy.form.lecdn_certificate_id.tooltip\": \"You can find it on LeCDN dashboard.\",\r\n  \"workflow_node.deploy.form.lecdn_client_id.label\": \"LeCDN user ID (Optional)\",\r\n  \"workflow_node.deploy.form.lecdn_client_id.placeholder\": \"Please enter LeCDN user ID\",\r\n  \"workflow_node.deploy.form.lecdn_client_id.tooltip\": \"You can find it on LeCDN dashboard. <br>Required when using administrator's authorization. It Must be the same as the user to which the certificate belongs.\",\r\n  \"workflow_node.deploy.form.local.guide\": \"If you are running Certimate in Docker, the \\\"Local\\\" refers to the container rather than the host.\",\r\n  \"workflow_node.deploy.form.local_format.label\": \"File format\",\r\n  \"workflow_node.deploy.form.local_format.placeholder\": \"Please select file format\",\r\n  \"workflow_node.deploy.form.local_format.option.pem.label\": \"PEM (*.pem, *.crt, *.key)\",\r\n  \"workflow_node.deploy.form.local_format.option.pfx.label\": \"PFX (*.pfx, *.p12)\",\r\n  \"workflow_node.deploy.form.local_format.option.jks.label\": \"JKS (*.jks)\",\r\n  \"workflow_node.deploy.form.local_key_path.label\": \"Private key file path\",\r\n  \"workflow_node.deploy.form.local_key_path.placeholder\": \"Please enter the local path for private key file\",\r\n  \"workflow_node.deploy.form.local_key_path.help\": \"Notes: It should include the full file path, not just the directory.\",\r\n  \"workflow_node.deploy.form.local_cert_path.label\": \"Certificate file path\",\r\n  \"workflow_node.deploy.form.local_cert_path.placeholder\": \"Please enter the local path for certificate file\",\r\n  \"workflow_node.deploy.form.local_cert_path.help\": \"Notes: It should include the full file path, not just the directory.\",\r\n  \"workflow_node.deploy.form.local_fullchaincert_path.label\": \"Bundled fullchain certificate file path\",\r\n  \"workflow_node.deploy.form.local_fullchaincert_path.placeholder\": \"Please enter the local path for bundled fullchain certificate\",\r\n  \"workflow_node.deploy.form.local_servercert_path.label\": \"Server certificate file path (Optional)\",\r\n  \"workflow_node.deploy.form.local_servercert_path.placeholder\": \"Please enter the local path for server certificate file\",\r\n  \"workflow_node.deploy.form.local_servercert_path.help\": \"Notes: It should include the full file path, not just the directory.\",\r\n  \"workflow_node.deploy.form.local_intermediacert_path.label\": \"Intermediate CA certificate file path (Optional)\",\r\n  \"workflow_node.deploy.form.local_intermediacert_path.placeholder\": \"Please enter the local path for intermediate CA certificate file\",\r\n  \"workflow_node.deploy.form.local_intermediacert_path.help\": \"Notes: It should include the full file path, not just the directory.\",\r\n  \"workflow_node.deploy.form.local_pfx_password.label\": \"PFX password\",\r\n  \"workflow_node.deploy.form.local_pfx_password.placeholder\": \"Please enter PFX password\",\r\n  \"workflow_node.deploy.form.local_pfx_password.tooltip\": \"For more information, see <a href=\\\"https://learn.microsoft.com/en-us/windows-hardware/drivers/install/personal-information-exchange---pfx--files\\\" target=\\\"_blank\\\">https://learn.microsoft.com/en-us/windows-hardware/drivers/install/personal-information-exchange---pfx--files</a>\",\r\n  \"workflow_node.deploy.form.local_jks_alias.label\": \"JKS alias\",\r\n  \"workflow_node.deploy.form.local_jks_alias.placeholder\": \"Please enter JKS alias\",\r\n  \"workflow_node.deploy.form.local_jks_alias.tooltip\": \"For more information, see <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.local_jks_keypass.label\": \"JKS key password\",\r\n  \"workflow_node.deploy.form.local_jks_keypass.placeholder\": \"Please enter JKS key password\",\r\n  \"workflow_node.deploy.form.local_jks_keypass.tooltip\": \"For more information, see <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.local_jks_storepass.label\": \"JKS store password\",\r\n  \"workflow_node.deploy.form.local_jks_storepass.placeholder\": \"Please enter JKS store password\",\r\n  \"workflow_node.deploy.form.local_jks_storepass.tooltip\": \"For more information, see <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.local_shell_env.label\": \"Shell\",\r\n  \"workflow_node.deploy.form.local_shell_env.placeholder\": \"Please select shell environment\",\r\n  \"workflow_node.deploy.form.local_shell_env.option.sh.label\": \"POSIX Bash (on Linux / macOS)\",\r\n  \"workflow_node.deploy.form.local_shell_env.option.cmd.label\": \"CMD (on Windows)\",\r\n  \"workflow_node.deploy.form.local_shell_env.option.powershell.label\": \"PowerShell (on Windows)\",\r\n  \"workflow_node.deploy.form.local_pre_command.label\": \"Pre-command (Optional)\",\r\n  \"workflow_node.deploy.form.local_pre_command.placeholder\": \"Please enter command to be executed before saving files\",\r\n  \"workflow_node.deploy.form.local_post_command.label\": \"Post-command (Optional)\",\r\n  \"workflow_node.deploy.form.local_post_command.placeholder\": \"Please enter command to be executed after saving files\",\r\n  \"workflow_node.deploy.form.local_preset_scripts.sh_backup_files\": \"POSIX Bash - Backup certificate files\",\r\n  \"workflow_node.deploy.form.local_preset_scripts.ps_backup_files\": \"PowerShell - Backup certificate files\",\r\n  \"workflow_node.deploy.form.local_preset_scripts.sh_reload_nginx\": \"POSIX Bash - Reload nginx\",\r\n  \"workflow_node.deploy.form.local_preset_scripts.ps_binding_iis\": \"PowerShell - Binding IIS\",\r\n  \"workflow_node.deploy.form.local_preset_scripts.ps_binding_netsh\": \"PowerShell - Binding netsh\",\r\n  \"workflow_node.deploy.form.local_preset_scripts.ps_binding_rdp\": \"PowerShell - Binding RDP\",\r\n  \"workflow_node.deploy.form.mohua_mvh_host_id.label\": \"Mohua Cloud virtual host ID\",\r\n  \"workflow_node.deploy.form.mohua_mvh_host_id.placeholder\": \"Please enter Mohua Cloud virtual host ID\",\r\n  \"workflow_node.deploy.form.mohua_mvh_host_id.tooltip\": \"For more information, see <a href=\\\"https://cloud.mhjz1.cn/service?groupid=328&language=english\\\" target=\\\"_blank\\\">https://cloud.mhjz1.cn/service?groupid=328&language=english</a>\",\r\n  \"workflow_node.deploy.form.mohua_mvh_domain_id.label\": \"Mohua Cloud virtual host domain ID\",\r\n  \"workflow_node.deploy.form.mohua_mvh_domain_id.placeholder\": \"Please enter Mohua Cloud virtual host domain ID\",\r\n  \"workflow_node.deploy.form.mohua_mvh_domain_id.tooltip\": \"For more information, see <a href=\\\"https://cloud.mhjz1.cn/service?groupid=328&language=english\\\" target=\\\"_blank\\\">https://cloud.mhjz1.cn/service?groupid=328&language=english</a>\",\r\n  \"workflow_node.deploy.form.netlify_resource_type.option.website.label\": \"Website\",\r\n  \"workflow_node.deploy.form.netlify_site_id.label\": \"Netlify website ID\",\r\n  \"workflow_node.deploy.form.netlify_site_id.placeholder\": \"Please enter Netlify website ID\",\r\n  \"workflow_node.deploy.form.netlify_site_id.tooltip\": \"For more information, see <a href=\\\"https://docs.netlify.com/api/get-started/#get-site\\\" target=\\\"_blank\\\">https://docs.netlify.com/api/get-started/#get-site</a>\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_resource_type.option.host.label\": \"Host\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_resource_type.option.certificate.label\": \"Certificate\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_match_pattern.label\": \"Host match pattern\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_match_pattern.placeholder\": \"Please select host match pattern\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_match_pattern.option.specified.label\": \"Specified ID\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_match_pattern.option.certsan.label\": \"via Certificate\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_type.label\": \"NPM host type\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_type.placeholder\": \"Please select NPM host type\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_type.option.proxy.label\": \"Proxy host\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_type.option.redirection.label\": \"Redirection host\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_type.option.stream.label\": \"Stream\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_type.option.dead.label\": \"404 host\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_id.label\": \"NPM host ID\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_id.placeholder\": \"Please enter NPM host ID\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_id.tooltip\": \"You can find it on NPM dashboard.\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_certificate_id.label\": \"NPM certificate ID\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_certificate_id.placeholder\": \"Please enter NPM certificate ID\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_certificate_id.tooltip\": \"You can find it on NPM dashboard.\",\r\n  \"workflow_node.deploy.form.proxmoxve.guide\": \"Requires Proxmox VE v5.2 or higher.\",\r\n  \"workflow_node.deploy.form.proxmoxve_node_name.label\": \"Proxmox VE cluster node name\",\r\n  \"workflow_node.deploy.form.proxmoxve_node_name.placeholder\": \"Please enter Proxmox VE cluster node name\",\r\n  \"workflow_node.deploy.form.proxmoxve_auto_restart.label\": \"Auto restart Proxmox VE after deployment\",\r\n  \"workflow_node.deploy.form.qiniu_cdn_domain.label\": \"Qiniu CDN domain\",\r\n  \"workflow_node.deploy.form.qiniu_cdn_domain.placeholder\": \"Please enter Qiniu CDN domain name\",\r\n  \"workflow_node.deploy.form.qiniu_kodo_bucket.label\": \"Qiniu Kodo bucket\",\r\n  \"workflow_node.deploy.form.qiniu_kodo_bucket.placeholder\": \"Please enter Qiniu Kodo bucket name\",\r\n  \"workflow_node.deploy.form.qiniu_kodo_domain.label\": \"Qiniu Kodo custom domain\",\r\n  \"workflow_node.deploy.form.qiniu_kodo_domain.placeholder\": \"Please enter Qiniu Kodo bucket custom domain name\",\r\n  \"workflow_node.deploy.form.qiniu_pili_hub.label\": \"Qiniu Pili hub\",\r\n  \"workflow_node.deploy.form.qiniu_pili_hub.placeholder\": \"Please enter Qiniu Pili hub name\",\r\n  \"workflow_node.deploy.form.qiniu_pili_hub.tooltip\": \"For more information, see <a href=\\\"https://portal.qiniu.com/hub\\\" target=\\\"_blank\\\">https://portal.qiniu.com/hub</a>\",\r\n  \"workflow_node.deploy.form.qiniu_pili_domain.label\": \"Qiniu Pili streaming domain\",\r\n  \"workflow_node.deploy.form.qiniu_pili_domain.placeholder\": \"Please enter Qiniu Pili streaming domain name\",\r\n  \"workflow_node.deploy.form.rainyun_rcdn_instance_id.label\": \"Rain Yun RCDN instance ID\",\r\n  \"workflow_node.deploy.form.rainyun_rcdn_instance_id.placeholder\": \"Please enter Rain Yun RCDN instance ID\",\r\n  \"workflow_node.deploy.form.rainyun_rcdn_instance_id.tooltip\": \"For more information, see <a href=\\\"https://app.rainyun.com/apps/rcdn/list\\\" target=\\\"_blank\\\">https://app.rainyun.com/apps/rcdn/list</a>\",\r\n  \"workflow_node.deploy.form.rainyun_rcdn_domain.label\": \"Rain Yun RCDN domain\",\r\n  \"workflow_node.deploy.form.rainyun_rcdn_domain.placeholder\": \"Please enter Rain Yun RCDN domain name\",\r\n  \"workflow_node.deploy.form.rainyun_sslcenter_certificate_id.label\": \"Rain Yun RCDN certificate ID\",\r\n  \"workflow_node.deploy.form.rainyun_sslcenter_certificate_id.placeholder\": \"Please enter Rain Yun RCDN certificate ID\",\r\n  \"workflow_node.deploy.form.rainyun_sslcenter_certificate_id.tooltip\": \"For more information, see <a href=\\\"https://app.rainyun.com/apps/ssl/list\\\" target=\\\"_blank\\\">https://app.rainyun.com/apps/ssl/list</a>\",\r\n  \"workflow_node.deploy.form.rainyun_sslcenter_certificate_id.help\": \"Notes: Leave it blank to import a new certificate; otherwise, to replace the existing one.\",\r\n  \"workflow_node.deploy.form.ratpanel.guide\": \"Requires AcePanel v2.5 or higher.\",\r\n  \"workflow_node.deploy.form.ratpanel_resource_type.option.website.label\": \"Website\",\r\n  \"workflow_node.deploy.form.ratpanel_resource_type.option.certificate.label\": \"Certificate\",\r\n  \"workflow_node.deploy.form.ratpanel_site_names.label\": \"AcePanel website names\",\r\n  \"workflow_node.deploy.form.ratpanel_site_names.placeholder\": \"Please enter AcePanel website names (separated by semicolons)\",\r\n  \"workflow_node.deploy.form.ratpanel_site_names.errmsg.invalid\": \"Please enter a valid AcePanel website name\",\r\n  \"workflow_node.deploy.form.ratpanel_site_names.help\": \"Notes: Multiple values should be separated by semicolons.\",\r\n  \"workflow_node.deploy.form.ratpanel_site_names.tooltip\": \"You can find it on AcePanel dashboard.\",\r\n  \"workflow_node.deploy.form.ratpanel_site_names.multiple_input_modal.title\": \"Change AcePanel website names\",\r\n  \"workflow_node.deploy.form.ratpanel_site_names.multiple_input_modal.placeholder\": \"Please enter AcePanel website name\",\r\n  \"workflow_node.deploy.form.ratpanel_certificate_id.label\": \"AcePanel certificate ID\",\r\n  \"workflow_node.deploy.form.ratpanel_certificate_id.placeholder\": \"Please enter AcePanel certificate ID\",\r\n  \"workflow_node.deploy.form.ratpanel_certificate_id.tooltip\": \"You can find it on AcePanel dashboard.\",\r\n  \"workflow_node.deploy.form.s3_region.label\": \"Object storage (S3-compatible) region\",\r\n  \"workflow_node.deploy.form.s3_region.placeholder\": \"Please enter region\",\r\n  \"workflow_node.deploy.form.s3_bucket.label\": \"Object storage (S3-compatible) bucket\",\r\n  \"workflow_node.deploy.form.s3_bucket.placeholder\": \"Please enter bucket name\",\r\n  \"workflow_node.deploy.form.s3_format.label\": \"File format\",\r\n  \"workflow_node.deploy.form.s3_format.placeholder\": \"Please select file format\",\r\n  \"workflow_node.deploy.form.s3_format.option.pem.label\": \"PEM (*.pem, *.crt, *.key)\",\r\n  \"workflow_node.deploy.form.s3_format.option.pfx.label\": \"PFX (*.pfx, *.p12)\",\r\n  \"workflow_node.deploy.form.s3_format.option.jks.label\": \"JKS (*.jks)\",\r\n  \"workflow_node.deploy.form.s3_key_object_key.label\": \"Private key file object key\",\r\n  \"workflow_node.deploy.form.s3_key_object_key.placeholder\": \"Please enter the object key for private key file\",\r\n  \"workflow_node.deploy.form.s3_cert_object_key.label\": \"Certificate file object key\",\r\n  \"workflow_node.deploy.form.s3_cert_object_key.placeholder\": \"Please enter the object key for certificate\",\r\n  \"workflow_node.deploy.form.s3_fullchaincert_object_key.label\": \"Bundled fullchain certificate object key\",\r\n  \"workflow_node.deploy.form.s3_fullchaincert_object_key.placeholder\": \"Please enter the remote path for bundled fullchain certificate\",\r\n  \"workflow_node.deploy.form.s3_servercert_object_key.label\": \"Server certificate object key (Optional)\",\r\n  \"workflow_node.deploy.form.s3_servercert_object_key.placeholder\": \"Please enter the object key for server certificate file\",\r\n  \"workflow_node.deploy.form.s3_servercert_object_key.help\": \"\",\r\n  \"workflow_node.deploy.form.s3_intermediacert_object_key.label\": \"Intermediate CA certificate object key (Optional)\",\r\n  \"workflow_node.deploy.form.s3_intermediacert_object_key.placeholder\": \"Please enter the object key for intermediate CA certificate file\",\r\n  \"workflow_node.deploy.form.s3_intermediacert_object_key.help\": \"\",\r\n  \"workflow_node.deploy.form.s3_pfx_password.label\": \"PFX password\",\r\n  \"workflow_node.deploy.form.s3_pfx_password.placeholder\": \"Please enter PFX password\",\r\n  \"workflow_node.deploy.form.s3_pfx_password.tooltip\": \"For more information, see <a href=\\\"https://learn.microsoft.com/en-us/windows-hardware/drivers/install/personal-information-exchange---pfx--files\\\" target=\\\"_blank\\\">https://learn.microsoft.com/en-us/windows-hardware/drivers/install/personal-information-exchange---pfx--files</a>\",\r\n  \"workflow_node.deploy.form.s3_jks_alias.label\": \"JKS alias\",\r\n  \"workflow_node.deploy.form.s3_jks_alias.placeholder\": \"Please enter JKS alias\",\r\n  \"workflow_node.deploy.form.s3_jks_alias.tooltip\": \"For more information, see <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.s3_jks_keypass.label\": \"JKS key password\",\r\n  \"workflow_node.deploy.form.s3_jks_keypass.placeholder\": \"Please enter JKS key password\",\r\n  \"workflow_node.deploy.form.s3_jks_keypass.tooltip\": \"For more information, see <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.s3_jks_storepass.label\": \"JKS store password\",\r\n  \"workflow_node.deploy.form.s3_jks_storepass.placeholder\": \"Please enter JKS store password\",\r\n  \"workflow_node.deploy.form.s3_jks_storepass.tooltip\": \"For more information, see <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.safeline.guide\": \"Requires SafeLine v6.6 or higher.\",\r\n  \"workflow_node.deploy.form.safeline_resource_type.option.certificate.label\": \"Certificate\",\r\n  \"workflow_node.deploy.form.safeline_certificate_id.label\": \"SafeLine certificate ID\",\r\n  \"workflow_node.deploy.form.safeline_certificate_id.placeholder\": \"Please enter SafeLine certificate ID\",\r\n  \"workflow_node.deploy.form.safeline_certificate_id.tooltip\": \"You can find it on SafeLine dashboard.\",\r\n  \"workflow_node.deploy.form.ssh_format.label\": \"File format\",\r\n  \"workflow_node.deploy.form.ssh_format.placeholder\": \"Please select file format\",\r\n  \"workflow_node.deploy.form.ssh_format.option.pem.label\": \"PEM (*.pem, *.crt, *.key)\",\r\n  \"workflow_node.deploy.form.ssh_format.option.pfx.label\": \"PFX (*.pfx, *.p12)\",\r\n  \"workflow_node.deploy.form.ssh_format.option.jks.label\": \"JKS (*.jks)\",\r\n  \"workflow_node.deploy.form.ssh_key_path.label\": \"Private key file path\",\r\n  \"workflow_node.deploy.form.ssh_key_path.placeholder\": \"Please enter the remote path for private key file\",\r\n  \"workflow_node.deploy.form.ssh_key_path.help\": \"Notes: It should include the full file path, not just the directory.\",\r\n  \"workflow_node.deploy.form.ssh_cert_path.label\": \"Certificate file path\",\r\n  \"workflow_node.deploy.form.ssh_cert_path.placeholder\": \"Please enter the remote path for certificate\",\r\n  \"workflow_node.deploy.form.ssh_cert_path.help\": \"Notes: It should include the full file path, not just the directory.\",\r\n  \"workflow_node.deploy.form.ssh_fullchaincert_path.label\": \"Bundled fullchain certificate file path\",\r\n  \"workflow_node.deploy.form.ssh_fullchaincert_path.placeholder\": \"Please enter the remote path for bundled fullchain certificate\",\r\n  \"workflow_node.deploy.form.ssh_servercert_path.label\": \"Server certificate file path (Optional)\",\r\n  \"workflow_node.deploy.form.ssh_servercert_path.placeholder\": \"Please enter the remote path for server certificate file\",\r\n  \"workflow_node.deploy.form.ssh_servercert_path.help\": \"Notes: It should include the full file path, not just the directory.\",\r\n  \"workflow_node.deploy.form.ssh_intermediacert_path.label\": \"Intermediate CA certificate file path (Optional)\",\r\n  \"workflow_node.deploy.form.ssh_intermediacert_path.placeholder\": \"Please enter the remote path for intermediate CA certificate file\",\r\n  \"workflow_node.deploy.form.ssh_intermediacert_path.help\": \"Notes: It should include the full file path, not just the directory.\",\r\n  \"workflow_node.deploy.form.ssh_pfx_password.label\": \"PFX password\",\r\n  \"workflow_node.deploy.form.ssh_pfx_password.placeholder\": \"Please enter PFX password\",\r\n  \"workflow_node.deploy.form.ssh_pfx_password.tooltip\": \"For more information, see <a href=\\\"https://learn.microsoft.com/en-us/windows-hardware/drivers/install/personal-information-exchange---pfx--files\\\" target=\\\"_blank\\\">https://learn.microsoft.com/en-us/windows-hardware/drivers/install/personal-information-exchange---pfx--files</a>\",\r\n  \"workflow_node.deploy.form.ssh_jks_alias.label\": \"JKS alias\",\r\n  \"workflow_node.deploy.form.ssh_jks_alias.placeholder\": \"Please enter JKS alias\",\r\n  \"workflow_node.deploy.form.ssh_jks_alias.tooltip\": \"For more information, see <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.ssh_jks_keypass.label\": \"JKS key password\",\r\n  \"workflow_node.deploy.form.ssh_jks_keypass.placeholder\": \"Please enter JKS key password\",\r\n  \"workflow_node.deploy.form.ssh_jks_keypass.tooltip\": \"For more information, see <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.ssh_jks_storepass.label\": \"JKS store password\",\r\n  \"workflow_node.deploy.form.ssh_jks_storepass.placeholder\": \"Please enter JKS store password\",\r\n  \"workflow_node.deploy.form.ssh_jks_storepass.tooltip\": \"For more information, see <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.ssh_pre_command.label\": \"Pre-command (Optional)\",\r\n  \"workflow_node.deploy.form.ssh_pre_command.placeholder\": \"Please enter command to be executed before uploading files\",\r\n  \"workflow_node.deploy.form.ssh_post_command.label\": \"Post-command (Optional)\",\r\n  \"workflow_node.deploy.form.ssh_post_command.placeholder\": \"Please enter command to be executed after uploading files\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.sh_backup_files\": \"POSIX Bash - Backup certificate files\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.ps_backup_files\": \"PowerShell - Backup certificate files\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.sh_reload_nginx\": \"POSIX Bash - Reload nginx\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.sh_replace_synologydsm_ssl\": \"POSIX Bash - Replace SynologyDSM SSL certificate\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.sh_replace_fnos_ssl\": \"POSIX Bash - Replace fnOS SSL certificate\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.sh_replace_qnap_ssl\": \"POSIX Bash - Replace QNAP SSL certificate\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.ps_binding_iis\": \"PowerShell - Binding IIS\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.ps_binding_netsh\": \"PowerShell - Binding netsh\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.ps_binding_rdp\": \"PowerShell - Binding RDP\",\r\n  \"workflow_node.deploy.form.ssh_use_scp.label\": \"Fallback to use SCP\",\r\n  \"workflow_node.deploy.form.ssh_use_scp.tooltip\": \"If the remote server does not support SFTP, please check this option to fallback to SCP.\",\r\n  \"workflow_node.deploy.form.synologydsm.guide\": \"Requires Synology DSM v6.0 or higher.\",\r\n  \"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.label\": \"Synology DSM certificate ID or description (Optional)\",\r\n  \"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.placeholder\": \"Please enter Synology DSM certificate ID or description\",\r\n  \"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.help\": \"Notes: Leave it blank to import a new certificate; otherwise, to replace the existing one.\",\r\n  \"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.tooltip\": \"You can find it on Synology DSM dashboard. <br><br>(Please open the browser devtools, and visit the \\\"Control Panel -> Security -> Certificate\\\" page to capture network requests, then you can get the certificate ID.)\",\r\n  \"workflow_node.deploy.form.synologydsm_is_default.label\": \"Set as default certificate and applied to all DSM services\",\r\n  \"workflow_node.deploy.form.tencentcloud_cdn_endpoint.label\": \"Tencent Cloud API endpoint (Optional)\",\r\n  \"workflow_node.deploy.form.tencentcloud_cdn_endpoint.placeholder\": \"Please enter Tencent Cloud CDN API endpoint (e.g. cdn.intl.tencentcloudapi.com)\",\r\n  \"workflow_node.deploy.form.tencentcloud_cdn_endpoint.tooltip\": \"<ul style=\\\"list-style: disc;\\\"><li><strong>cdn.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>cdn.tencentcloudapi.com</strong> for Tencent Cloud in China</li></ul>\",\r\n  \"workflow_node.deploy.form.tencentcloud_cdn_domain.label\": \"Tencent Cloud CDN domain\",\r\n  \"workflow_node.deploy.form.tencentcloud_cdn_domain.placeholder\": \"Please enter Tencent Cloud CDN domain name\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_endpoint.label\": \"Tencent Cloud API endpoint (Optional)\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_endpoint.placeholder\": \"Please enter Tencent Cloud CLB API endpoint (e.g. clb.intl.tencentcloudapi.com)\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_endpoint.tooltip\": \"<ul style=\\\"list-style: disc;\\\"><li><strong>clb.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>clb.tencentcloudapi.com</strong> for Tencent Cloud in China</li></ul>\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_region.label\": \"Tencent Cloud region\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_region.placeholder\": \"Please enter Tencent Cloud CLB region (e.g. ap-guangzhou)\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_region.tooltip\": \"For more information, see <a href=\\\"https://www.tencentcloud.com/document/product/214/13629\\\" target=\\\"_blank\\\">https://www.tencentcloud.com/document/product/214/13629</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_resource_type.option.loadbalancer.label\": \"CLB instance\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_resource_type.option.listener.label\": \"CLB listener\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_resource_type.option.ruledomain.label\": \"CLB rule domain\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_loadbalancer_id.label\": \"Tencent Cloud CLB instance ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_loadbalancer_id.placeholder\": \"Please enter Tencent Cloud CLB instance ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_loadbalancer_id.tooltip\": \"For more information, see <a href=\\\"https://console.tencentcloud.com/clb\\\" target=\\\"_blank\\\">https://console.tencentcloud.com/clb</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_listener_id.label\": \"Tencent Cloud CLB listener ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_listener_id.placeholder\": \"Please enter Tencent Cloud CLB listener ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_listener_id.tooltip\": \"For more information, see <a href=\\\"https://console.tencentcloud.com/clb\\\" target=\\\"_blank\\\">https://console.tencentcloud.com/clb</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_ruledomain.label\": \"Tencent Cloud CLB domain\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_ruledomain.placeholder\": \"Please enter Tencent Cloud CLB domain name\",\r\n  \"workflow_node.deploy.form.tencentcloud_cos_region.label\": \"Tencent Cloud region\",\r\n  \"workflow_node.deploy.form.tencentcloud_cos_region.placeholder\": \"Please enter Tencent Cloud COS region (e.g. ap-guangzhou)\",\r\n  \"workflow_node.deploy.form.tencentcloud_cos_region.tooltip\": \"For more information, see <a href=\\\"https://www.tencentcloud.com/document/product/436/6224\\\" target=\\\"_blank\\\">https://www.tencentcloud.com/document/product/436/6224</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_cos_bucket.label\": \"Tencent Cloud COS bucket\",\r\n  \"workflow_node.deploy.form.tencentcloud_cos_bucket.placeholder\": \"Please enter Tencent Cloud COS bucket name\",\r\n  \"workflow_node.deploy.form.tencentcloud_cos_domain.label\": \"Tencent Cloud COS custom domain\",\r\n  \"workflow_node.deploy.form.tencentcloud_cos_domain.placeholder\": \"Please enter Tencent Cloud COS bucket custom domain name\",\r\n  \"workflow_node.deploy.form.tencentcloud_css_endpoint.label\": \"Tencent Cloud API endpoint (Optional)\",\r\n  \"workflow_node.deploy.form.tencentcloud_css_endpoint.placeholder\": \"Please enter Tencent Cloud CSS API endpoint (e.g. live.intl.tencentcloudapi.com)\",\r\n  \"workflow_node.deploy.form.tencentcloud_css_endpoint.tooltip\": \"<ul style=\\\"list-style: disc;\\\"><li><strong>live.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>live.tencentcloudapi.com</strong> for Tencent Cloud in China</li></ul>\",\r\n  \"workflow_node.deploy.form.tencentcloud_css_domain.label\": \"Tencent Cloud CSS play domain\",\r\n  \"workflow_node.deploy.form.tencentcloud_css_domain.placeholder\": \"Please enter Tencent Cloud CSS play domain name\",\r\n  \"workflow_node.deploy.form.tencentcloud_ecdn_endpoint.label\": \"Tencent Cloud API endpoint (Optional)\",\r\n  \"workflow_node.deploy.form.tencentcloud_ecdn_endpoint.placeholder\": \"Please enter Tencent Cloud ECDN API endpoint (e.g. cdn.intl.tencentcloudapi.com)\",\r\n  \"workflow_node.deploy.form.tencentcloud_ecdn_endpoint.tooltip\": \"<ul style=\\\"list-style: disc;\\\"><li><strong>cdn.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>cdn.tencentcloudapi.com</strong> for Tencent Cloud in China</li></ul>\",\r\n  \"workflow_node.deploy.form.tencentcloud_ecdn_domain.label\": \"Tencent Cloud ECDN domain\",\r\n  \"workflow_node.deploy.form.tencentcloud_ecdn_domain.placeholder\": \"Please enter Tencent Cloud ECDN domain name\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_endpoint.label\": \"Tencent Cloud API endpoint (Optional)\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_endpoint.placeholder\": \"Please enter Tencent Cloud EdgeOne API endpoint (e.g. teo.intl.tencentcloudapi.com)\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_endpoint.tooltip\": \"<ul style=\\\"list-style: disc;\\\"><li><strong>cdn.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>teo.tencentcloudapi.com</strong> for Tencent Cloud in China</li></ul>\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_zone_id.label\": \"Tencent Cloud EdgeOne zone ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder\": \"Please enter Tencent Cloud EdgeOne zone ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_zone_id.tooltip\": \"For more information, see <a href=\\\"https://console.tencentcloud.com/edgeone\\\" target=\\\"_blank\\\">https://console.tencentcloud.com/edgeone</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_domains.label\": \"Tencent Cloud EdgeOne domains\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_domains.placeholder\": \"Please enter Tencent Cloud EdgeOne domain names (separated by semicolons)\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_domains.help\": \"Notes: Multiple domains should be separated by semicolons.\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.title\": \"Change Tencent Cloud EdgeOne domain\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.placeholder\": \"Please enter Tencent Cloud EdgeOne domain name\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.label\": \"Multiple SSL certificates\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.help\": \"Notes: Each domain name supports one RSA certificate and one ECC certificate at most.\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.suffix\": \"to retain other certificates with different algorithms.\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.on\": \"Allow\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.off\": \"Disallow\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_endpoint.label\": \"Tencent Cloud API endpoint (Optional)\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_endpoint.placeholder\": \"Please enter Tencent Cloud GAAP API endpoint (e.g. gaap.intl.tencentcloudapi.com)\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_endpoint.tooltip\": \"<ul style=\\\"list-style: disc;\\\"><li><strong>gaap.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>gaap.tencentcloudapi.com</strong> for Tencent Cloud in China</li></ul>\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_resource_type.option.listener.label\": \"GAAP listener\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_proxy_id.label\": \"Tencent Cloud GAAP proxy ID (Optional)\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_proxy_id.placeholder\": \"Please enter Tencent Cloud GAAP proxy ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_proxy_id.tooltip\": \"For more information, see <a href=\\\"https://console.tencentcloud.com/gaap\\\" target=\\\"_blank\\\">https://console.tencentcloud.com/gaap</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_listener_id.label\": \"Tencent Cloud GAAP listener ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_listener_id.placeholder\": \"Please enter Tencent Cloud GAAP listener ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_listener_id.tooltip\": \"For more information, see <a href=\\\"https://console.tencentcloud.com/gaap\\\" target=\\\"_blank\\\">https://console.tencentcloud.com/gaap</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_scf_endpoint.label\": \"Tencent Cloud API endpoint (Optional)\",\r\n  \"workflow_node.deploy.form.tencentcloud_scf_endpoint.placeholder\": \"Please enter Tencent Cloud SCF API endpoint (e.g. scf.intl.tencentcloudapi.com)\",\r\n  \"workflow_node.deploy.form.tencentcloud_scf_endpoint.tooltip\": \"<ul style=\\\"list-style: disc;\\\"><li><strong>scf.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>scf.tencentcloudapi.com</strong> for Tencent Cloud in China</li></ul>\",\r\n  \"workflow_node.deploy.form.tencentcloud_scf_region.label\": \"Tencent Cloud region\",\r\n  \"workflow_node.deploy.form.tencentcloud_scf_region.placeholder\": \"Please enter Tencent Cloud SCF region (e.g. ap-guangzhou)\",\r\n  \"workflow_node.deploy.form.tencentcloud_scf_region.tooltip\": \"For more information, see <a href=\\\"https://www.tencentcloud.com/document/product/583/17299\\\" target=\\\"_blank\\\">https://www.tencentcloud.com/document/product/583/17299</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_scf_domain.label\": \"Tencent Cloud SCF domain\",\r\n  \"workflow_node.deploy.form.tencentcloud_scf_domain.placeholder\": \"Please enter Tencent Cloud SCF domain name\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssl_endpoint.label\": \"Tencent Cloud API endpoint (Optional)\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssl_endpoint.placeholder\": \"Please enter Tencent Cloud SSL API endpoint (e.g. ssl.intl.tencentcloudapi.com)\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssl_endpoint.tooltip\": \"<ul style=\\\"list-style: disc;\\\"><li><strong>ssl.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>ssl.tencentcloudapi.com</strong> for Tencent Cloud in China</li></ul>\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy.guide\": \"TIPS: This will invoke Tencent Cloud OpenAPI <em>DeployCertificateInstance</em> to create an asynchronously deployment task. You need to go to the Tencent Cloud console to check the actual deployment results by yourself.\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_endpoint.label\": \"Tencent Cloud API endpoint (Optional)\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_endpoint.placeholder\": \"Please enter Tencent Cloud SSL API endpoint (e.g. ssl.intl.tencentcloudapi.com)\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_endpoint.tooltip\": \"<ul style=\\\"list-style: disc;\\\"><li><strong>ssl.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>ssl.tencentcloudapi.com</strong> for Tencent Cloud in China</li></ul>\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_region.label\": \"Tencent Cloud region\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_region.placeholder\": \"Please enter Tencent Cloud service region (e.g. ap-guangzhou)\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_region.tooltip\": \"For more information, see <a href=\\\"https://www.tencentcloud.com/document/product/1007/36573\\\" target=\\\"_blank\\\">https://www.tencentcloud.com/document/product/1007/36573</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_product.label\": \"Tencent Cloud resource product\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_product.placeholder\": \"Please enter Tencent Cloud resource product\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_product.tooltip\": \"For more information, see <a href=\\\"https://cloud.tencent.com/document/product/400/91667\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/400/91667</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.label\": \"Tencent Cloud resource IDs\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.placeholder\": \"Please enter Tencent Cloud resource IDs (separated by semicolons)\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.errmsg.invalid\": \"Please enter a valid Tencent Cloud resource ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.help\": \"Notes: Multiple values should be separated by semicolons.\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.tooltip\": \"For more information, see <a href=\\\"https://cloud.tencent.com/document/product/400/91667\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/400/91667</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.multiple_input_modal.title\": \"Change Tencent Cloud resource IDs\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.multiple_input_modal.placeholder\": \"Please enter Tencent Cloud resouce ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate.guide\": \"TIPS: This will invoke Tencent Cloud OpenAPI <em>UpdateCertificateInstance</em> or <em>UploadUpdateCertificateInstance</em> to create an asynchronously deployment task. You need to go to the Tencent Cloud console to check the actual deployment results by yourself.\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_endpoint.label\": \"Tencent Cloud API endpoint (Optional)\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_endpoint.placeholder\": \"Please enter Tencent Cloud SSL API endpoint (e.g. ssl.intl.tencentcloudapi.com)\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_endpoint.tooltip\": \"<ul style=\\\"list-style: disc;\\\"><li><strong>ssl.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>ssl.tencentcloudapi.com</strong> for Tencent Cloud in China</li></ul>\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_certificate_id.label\": \"Tencent Cloud SSL certificate ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_certificate_id.placeholder\": \"Please enter Tencent Cloud SSL certificate ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_certificate_id.tooltip\": \"For more information, see <a href=\\\"https://console.cloud.tencent.com/certoverview\\\" target=\\\"_blank\\\">https://console.cloud.tencent.com/certoverview</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.label\": \"Tencent Cloud resource products\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.placeholder\": \"Please enter Tencent Cloud resource products (separated by semicolons)\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.help\": \"Notes: Multiple values should be separated by semicolons.\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.tooltip\": \"For more information, see <a href=\\\"https://www.tencentcloud.com/document/product/1007/57981\\\" target=\\\"_blank\\\">https://www.tencentcloud.com/document/product/1007/57981</a> or <a href=\\\"https://www.tencentcloud.com/document/product/1007/70503\\\" target=\\\"_blank\\\">https://www.tencentcloud.com/document/product/1007/70503</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.multiple_input_modal.title\": \"Change Tencent Cloud resource products\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.multiple_input_modal.placeholder\": \"Please enter Tencent Cloud resource product\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.label\": \"Tencent Cloud resource regions (Optional)\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.placeholder\": \"Please enter Tencent Cloud resource regions (separated by semicolons)\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.help\": \"Notes: Multiple values should be separated by semicolons.\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.tooltip\": \"For more information, see <a href=\\\"https://www.tencentcloud.com/document/product/1007/57981\\\" target=\\\"_blank\\\">https://www.tencentcloud.com/document/product/1007/57981</a> or <a href=\\\"https://www.tencentcloud.com/document/product/1007/70503\\\" target=\\\"_blank\\\">https://www.tencentcloud.com/document/product/1007/70503</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.multiple_input_modal.title\": \"Change Tencent Cloud resource regions\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.multiple_input_modal.placeholder\": \"Please enter Tencent Cloud resource region\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_is_replaced.label\": \"Renewal certificate (certificate ID unchanged)\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_is_replaced.tooltip\": \"When unchecked, it will invoke <em>UpdateCertificateInstance</em>; otherwise, it will invoke <em>UploadUpdateCertificateInstance</em>.\",\r\n  \"workflow_node.deploy.form.tencentcloud_vod_endpoint.label\": \"Tencent Cloud API endpoint (Optional)\",\r\n  \"workflow_node.deploy.form.tencentcloud_vod_endpoint.placeholder\": \"Please enter Tencent Cloud VOD API endpoint (e.g. vod.intl.tencentcloudapi.com)\",\r\n  \"workflow_node.deploy.form.tencentcloud_vod_endpoint.tooltip\": \"<ul style=\\\"list-style: disc;\\\"><li><strong>vod.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>vod.tencentcloudapi.com</strong> for Tencent Cloud in China</li></ul>\",\r\n  \"workflow_node.deploy.form.tencentcloud_vod_sub_app_id.label\": \"Tencent Cloud VOD App ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_vod_sub_app_id.placeholder\": \"Please enter Tencent Cloud VOD App ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_vod_sub_app_id.tooltip\": \"For more information, see <a href=\\\"https://console.tencentcloud.com/vod\\\" target=\\\"_blank\\\">https://console.tencentcloud.com/vod</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_vod_domain.label\": \"Tencent Cloud VOD domain\",\r\n  \"workflow_node.deploy.form.tencentcloud_vod_domain.placeholder\": \"Please enter Tencent Cloud VOD domain name\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_endpoint.label\": \"Tencent Cloud API endpoint (Optional)\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_endpoint.placeholder\": \"Please enter Tencent Cloud WAF API endpoint (e.g. waf.intl.tencentcloudapi.com)\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_endpoint.tooltip\": \"<ul style=\\\"list-style: disc;\\\"><li><strong>waf.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>waf.tencentcloudapi.com</strong> for Tencent Cloud in China</li></ul>\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_region.label\": \"Tencent Cloud region\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_region.placeholder\": \"Please enter Tencent Cloud WAF region (e.g. ap-guangzhou)\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_region.tooltip\": \"For more information, see <a href=\\\"https://www.tencentcloud.com/document/product/627/38085\\\" target=\\\"_blank\\\">https://www.tencentcloud.com/document/product/627/38085</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_domain.label\": \"Tencent Cloud WAF domain\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_domain.placeholder\": \"Please enter Tencent Cloud WAF domain name\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_domain_id.label\": \"Tencent Cloud WAF domain ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_domain_id.placeholder\": \"Please enter Tencent Cloud WAF domain ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_domain_id.tooltip\": \"For more information, see <a href=\\\"https://console.tencentcloud.com/waf\\\" target=\\\"_blank\\\">https://console.tencentcloud.com/waf</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_instance_id.label\": \"Tencent Cloud WAF instance ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_instance_id.placeholder\": \"Please enter Tencent Cloud WAF instance ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_instance_id.tooltip\": \"For more information, see <a href=\\\"https://console.tencentcloud.com/waf\\\" target=\\\"_blank\\\">https://console.tencentcloud.com/waf</a>\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_region.label\": \"UCloud region\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_region.placeholder\": \"Please enter UCloud ALB region (e.g. cn-bj2)\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_region.tooltip\": \"For more information, see <a href=\\\"https://www.ucloud-global.com/en/docs/api/summary/regionlist\\\" target=\\\"_blank\\\">https://www.ucloud-global.com/en/docs/api/summary/regionlist</a>\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_resource_type.option.loadbalancer.label\": \"UALB load balancer\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_resource_type.option.listener.label\": \"UALB listener\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_loadbalancer_id.label\": \"UCloud UALB load balancer ID\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_loadbalancer_id.placeholder\": \"Please enter UCloud ALB load balancer ID\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_loadbalancer_id.tooltip\": \"For more information, see <a href=\\\"https://console.ucloud-global.com/ulb/alb\\\" target=\\\"_blank\\\">https://console.ucloud-global.com/ulb/alb</a>\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_listener_id.label\": \"UCloud UALB listener ID\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_listener_id.placeholder\": \"Please enter UCloud ALB listener ID\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_listener_id.tooltip\": \"For more information, see <a href=\\\"https://console.ucloud-global.com/ulb/alb\\\" target=\\\"_blank\\\">https://console.ucloud-global.com/ulb/alb</a>\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_snidomain.label\": \"UCloud UALB SNI domain (Optional)\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_snidomain.placeholder\": \"Please enter UCloud UALB SNI domain name\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_snidomain.help\": \"Notes: Leave it blank to set the default certificate; otherwise, to set the extension one for SNI.\",\r\n  \"workflow_node.deploy.form.ucloud_ucdn_domain_id.label\": \"UCloud UCDN domain ID\",\r\n  \"workflow_node.deploy.form.ucloud_ucdn_domain_id.placeholder\": \"Please enter UCloud UCDN domain ID\",\r\n  \"workflow_node.deploy.form.ucloud_ucdn_domain_id.tooltip\": \"For more information, see <a href=\\\"https://console.ucloud-global.com/ucdn\\\" target=\\\"_blank\\\">https://console.ucloud-global.com/ucdn</a>\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_region.label\": \"UCloud region\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_region.placeholder\": \"Please enter UCloud UCLB region (e.g. cn-bj2)\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_region.tooltip\": \"For more information, see <a href=\\\"https://www.ucloud-global.com/en/docs/api/summary/regionlist\\\" target=\\\"_blank\\\">https://www.ucloud-global.com/en/docs/api/summary/regionlist</a>\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_resource_type.option.loadbalancer.label\": \"UCLB load balancer\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_resource_type.option.vserver.label\": \"UCLB VServer\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_loadbalancer_id.label\": \"UCloud UCLB load balancer ID\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_loadbalancer_id.placeholder\": \"Please enter UCloud UCLB load balancer ID\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_loadbalancer_id.tooltip\": \"For more information, see <a href=\\\"https://console.ucloud-global.com/ulb/ulb\\\" target=\\\"_blank\\\">https://console.ucloud-global.com/ulb/ulb</a>\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_vserver_id.label\": \"UCloud UCLB VServer ID\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_vserver_id.placeholder\": \"Please enter UCloud UCLB VServer ID\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_vserver_id.tooltip\": \"For more information, see <a href=\\\"https://console.ucloud-global.com/ulb/ulb\\\" target=\\\"_blank\\\">https://console.ucloud-global.com/ulb/ulb</a>\",\r\n  \"workflow_node.deploy.form.ucloud_upathx_accelerator_id.label\": \"UCloud UPathX accelerator ID\",\r\n  \"workflow_node.deploy.form.ucloud_upathx_accelerator_id.placeholder\": \"Please enter UCloud UPathX accelerator ID\",\r\n  \"workflow_node.deploy.form.ucloud_upathx_accelerator_id.tooltip\": \"For more information, see <a href=\\\"https://console.ucloud-global.com/upathx/accelerate\\\" target=\\\"_blank\\\">https://console.ucloud-global.com/upathx/accelerate</a>\",\r\n  \"workflow_node.deploy.form.ucloud_upathx_listener_port.label\": \"UCloud UPathX listener port\",\r\n  \"workflow_node.deploy.form.ucloud_upathx_listener_port.placeholder\": \"Please enter UCloud UPathX listener port\",\r\n  \"workflow_node.deploy.form.ucloud_upathx_listener_port.tooltip\": \"For more information, see <a href=\\\"https://console.ucloud-global.com/upathx/accelerate\\\" target=\\\"_blank\\\">https://console.ucloud-global.com/upathx/accelerate</a>\",\r\n  \"workflow_node.deploy.form.ucloud_us3_region.label\": \"UCloud region\",\r\n  \"workflow_node.deploy.form.ucloud_us3_region.placeholder\": \"Please enter UCloud US3 region (e.g. cn-bj2)\",\r\n  \"workflow_node.deploy.form.ucloud_us3_region.tooltip\": \"For more information, see <a href=\\\"https://www.ucloud-global.com/en/docs/api/summary/regionlist\\\" target=\\\"_blank\\\">https://www.ucloud-global.com/en/docs/api/summary/regionlist</a>\",\r\n  \"workflow_node.deploy.form.ucloud_us3_bucket.label\": \"UCloud US3 bucket\",\r\n  \"workflow_node.deploy.form.ucloud_us3_bucket.placeholder\": \"Please enter UCloud US3 bucket name\",\r\n  \"workflow_node.deploy.form.ucloud_us3_domain.label\": \"UCloud US3 custom domain\",\r\n  \"workflow_node.deploy.form.ucloud_us3_domain.placeholder\": \"Please enter UCloud US3 bucket custom domain name\",\r\n  \"workflow_node.deploy.form.ucloud_uewaf_domain.label\": \"UCloud UEWAF domain\",\r\n  \"workflow_node.deploy.form.ucloud_uewaf_domain.placeholder\": \"Please enter UCloud UEWAF domain name\",\r\n  \"workflow_node.deploy.form.unicloud_webhost.guide\": \"This uses webpage simulator login and does not guarantee stability. If there are any changes to the uniCloud, please create a GitHub Issue.\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_space_provider.label\": \"uniCloud space provider\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_space_provider.placeholder\": \"Please select uniCloud space provider\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_space_provider.option.aliyun.label\": \"Alibaba Cloud\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_space_provider.option.tencent.label\": \"Tencent Cloud\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_space_id.label\": \"uniCloud space ID\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_space_id.placeholder\": \"uniCloud space ID\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_space_id.tooltip\": \"For more information, see <a href=\\\"https://doc.dcloud.net.cn/uniCloud/concepts/space.html\\\" target=\\\"_blank\\\">https://doc.dcloud.net.cn/uniCloud/concepts/space.html</a>\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_domain.label\": \"uniCloud Web host domain\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_domain.placeholder\": \"uniCloud Web host domain\",\r\n  \"workflow_node.deploy.form.upyun_cdn.guide\": \"This uses webpage simulator login and does not guarantee stability. If there are any changes to the UPYUN, please create a GitHub Issue.\",\r\n  \"workflow_node.deploy.form.upyun_cdn_domain.label\": \"UPYUN CDN domain\",\r\n  \"workflow_node.deploy.form.upyun_cdn_domain.placeholder\": \"Please enter UPYUN CDN domain name\",\r\n  \"workflow_node.deploy.form.upyun_file.guide\": \"This uses webpage simulator login and does not guarantee stability. If there are any changes to the UPYUN, please create a GitHub Issue.\",\r\n  \"workflow_node.deploy.form.upyun_file_bucket.label\": \"UPYUN USS bucket\",\r\n  \"workflow_node.deploy.form.upyun_file_bucket.placeholder\": \"Please enter UPYUN USS bucket name\",\r\n  \"workflow_node.deploy.form.upyun_file_domain.label\": \"UPYUN USS custom domain\",\r\n  \"workflow_node.deploy.form.upyun_file_domain.placeholder\": \"Please enter UPYUN USS bucket custom domain name\",\r\n  \"workflow_node.deploy.form.volcengine_alb_region.label\": \"VolcEngine region\",\r\n  \"workflow_node.deploy.form.volcengine_alb_region.placeholder\": \"Please enter VolcEngine ALB region (e.g. cn-beijing)\",\r\n  \"workflow_node.deploy.form.volcengine_alb_region.tooltip\": \"For more information, see <a href=\\\"https://www.volcengine.com/docs/6767/127501\\\" target=\\\"_blank\\\">https://www.volcengine.com/docs/6767/127501</a>\",\r\n  \"workflow_node.deploy.form.volcengine_alb_resource_type.option.loadbalancer.label\": \"ALB load balancer\",\r\n  \"workflow_node.deploy.form.volcengine_alb_resource_type.option.listener.label\": \"ALB listener\",\r\n  \"workflow_node.deploy.form.volcengine_alb_loadbalancer_id.label\": \"VolcEngine ALB load balancer ID\",\r\n  \"workflow_node.deploy.form.volcengine_alb_loadbalancer_id.placeholder\": \"Please enter VolcEngine ALB load balancer ID\",\r\n  \"workflow_node.deploy.form.volcengine_alb_loadbalancer_id.tooltip\": \"For more information, see <a href=\\\"https://console.volcengine.com/alb\\\" target=\\\"_blank\\\">https://console.volcengine.com/alb</a>\",\r\n  \"workflow_node.deploy.form.volcengine_alb_listener_id.label\": \"VolcEngine ALB listener ID\",\r\n  \"workflow_node.deploy.form.volcengine_alb_listener_id.placeholder\": \"Please enter VolcEngine ALB listener ID\",\r\n  \"workflow_node.deploy.form.volcengine_alb_listener_id.tooltip\": \"For more information, see <a href=\\\"https://console.volcengine.com/alb\\\" target=\\\"_blank\\\">https://console.volcengine.com/alb</a>\",\r\n  \"workflow_node.deploy.form.volcengine_alb_snidomain.label\": \"VolcEngine ALB SNI domain (Optional)\",\r\n  \"workflow_node.deploy.form.volcengine_alb_snidomain.placeholder\": \"Please enter VolcEngine ALB SNI domain name\",\r\n  \"workflow_node.deploy.form.volcengine_alb_snidomain.help\": \"Notes: Leave it blank to set the default certificate; otherwise, to set the extension one for SNI.\",\r\n  \"workflow_node.deploy.form.volcengine_cdn_domain.label\": \"VolcEngine CDN domain\",\r\n  \"workflow_node.deploy.form.volcengine_cdn_domain.placeholder\": \"Please enter VolcEngine CDN domain name\",\r\n  \"workflow_node.deploy.form.volcengine_certcenter_region.label\": \"VolcEngine region\",\r\n  \"workflow_node.deploy.form.volcengine_certcenter_region.placeholder\": \"Please enter VolcEngine Certificate Center region (e.g. cn-beijing)\",\r\n  \"workflow_node.deploy.form.volcengine_clb_region.label\": \"VolcEngine region\",\r\n  \"workflow_node.deploy.form.volcengine_clb_region.placeholder\": \"Please enter VolcEngine CLB region (e.g. cn-beijing)\",\r\n  \"workflow_node.deploy.form.volcengine_clb_region.tooltip\": \"For more information, see <a href=\\\"https://www.volcengine.com/docs/6406/74892\\\" target=\\\"_blank\\\">https://www.volcengine.com/docs/6406/74892</a>\",\r\n  \"workflow_node.deploy.form.volcengine_clb_resource_type.option.loadbalancer.label\": \"CLB load balancer\",\r\n  \"workflow_node.deploy.form.volcengine_clb_resource_type.option.listener.label\": \"CLB listener\",\r\n  \"workflow_node.deploy.form.volcengine_clb_loadbalancer_id.label\": \"VolcEngine CLB load balancer ID\",\r\n  \"workflow_node.deploy.form.volcengine_clb_loadbalancer_id.placeholder\": \"Please enter VolcEngine CLB load balancer ID\",\r\n  \"workflow_node.deploy.form.volcengine_clb_loadbalancer_id.tooltip\": \"For more information, see <a href=\\\"https://console.volcengine.com/clb/LoadBalancer\\\" target=\\\"_blank\\\">https://console.volcengine.com/clb/LoadBalancer</a>\",\r\n  \"workflow_node.deploy.form.volcengine_clb_listener_id.label\": \"VolcEngine CLB listener ID\",\r\n  \"workflow_node.deploy.form.volcengine_clb_listener_id.placeholder\": \"Please enter VolcEngine CLB listener ID\",\r\n  \"workflow_node.deploy.form.volcengine_clb_listener_id.tooltip\": \"For more information, see <a href=\\\"https://console.volcengine.com/clb/LoadBalancer\\\" target=\\\"_blank\\\">https://console.volcengine.com/clb/LoadBalancer</a>\",\r\n  \"workflow_node.deploy.form.volcengine_dcdn_domain.label\": \"VolcEngine DCDN domain\",\r\n  \"workflow_node.deploy.form.volcengine_dcdn_domain.placeholder\": \"Please enter VolcEngine DCDN domain name\",\r\n  \"workflow_node.deploy.form.volcengine_imagex_region.label\": \"VolcEngine region\",\r\n  \"workflow_node.deploy.form.volcengine_imagex_region.placeholder\": \"Please enter VolcEngine ImageX region (e.g. cn-north-1)\",\r\n  \"workflow_node.deploy.form.volcengine_imagex_region.tooltip\": \"For more information, see <a href=\\\"https://www.volcengine.com/docs/508/23757\\\" target=\\\"_blank\\\">https://www.volcengine.com/docs/508/23757</a>\",\r\n  \"workflow_node.deploy.form.volcengine_imagex_service_id.label\": \"VolcEngine ImageX service ID\",\r\n  \"workflow_node.deploy.form.volcengine_imagex_service_id.placeholder\": \"Please enter VolcEngine ImageX service ID\",\r\n  \"workflow_node.deploy.form.volcengine_imagex_service_id.tooltip\": \"For more information, see <a href=\\\"https://console.volcengine.com/imagex\\\" target=\\\"_blank\\\">https://console.volcengine.com/imagex</a>\",\r\n  \"workflow_node.deploy.form.volcengine_imagex_domain.label\": \"VolcEngine ImageX custom domain\",\r\n  \"workflow_node.deploy.form.volcengine_imagex_domain.placeholder\": \"Please enter VolcEngine ImageX custom domain name\",\r\n  \"workflow_node.deploy.form.volcengine_live_domain.label\": \"VolcEngine Live streaming domain\",\r\n  \"workflow_node.deploy.form.volcengine_live_domain.placeholder\": \"Please enter VolcEngine Live streaming domain name\",\r\n  \"workflow_node.deploy.form.volcengine_tos_region.label\": \"VolcEngine region\",\r\n  \"workflow_node.deploy.form.volcengine_tos_region.placeholder\": \"Please enter VolcEngine TOS region (e.g. cn-beijing)\",\r\n  \"workflow_node.deploy.form.volcengine_tos_region.tooltip\": \"For more information, see <a href=\\\"https://www.volcengine.com/docs/6349/107356\\\" target=\\\"_blank\\\">https://www.volcengine.com/docs/6349/107356</a>\",\r\n  \"workflow_node.deploy.form.volcengine_tos_bucket.label\": \"VolcEngine TOS bucket\",\r\n  \"workflow_node.deploy.form.volcengine_tos_bucket.placeholder\": \"Please enter VolcEngine TOS bucket name\",\r\n  \"workflow_node.deploy.form.volcengine_tos_domain.label\": \"VolcEngine TOS custom domain\",\r\n  \"workflow_node.deploy.form.volcengine_tos_domain.placeholder\": \"Please enter VolcEngine TOS bucket custom domain name\",\r\n  \"workflow_node.deploy.form.volcengine_vod_space_name.label\": \"VolcEngine VOD space name\",\r\n  \"workflow_node.deploy.form.volcengine_vod_space_name.placeholder\": \"Please enter VolcEngine VOD space name\",\r\n  \"workflow_node.deploy.form.volcengine_vod_space_name.tooltip\": \"For more information, see <a href=\\\"https://console.volcengine.com/vod/overview\\\" target=\\\"_blank\\\">https://console.volcengine.com/vod/overview</a>\",\r\n  \"workflow_node.deploy.form.volcengine_vod_domain_type.label\": \"VolcEngine VOD domain type\",\r\n  \"workflow_node.deploy.form.volcengine_vod_domain_type.placeholder\": \"Please select VolcEngine VOD domain type\",\r\n  \"workflow_node.deploy.form.volcengine_vod_domain_type.option.play.label\": \"Play\",\r\n  \"workflow_node.deploy.form.volcengine_vod_domain_type.option.image.label\": \"Image\",\r\n  \"workflow_node.deploy.form.volcengine_vod_domain.label\": \"VolcEngine VOD domain\",\r\n  \"workflow_node.deploy.form.volcengine_vod_domain.placeholder\": \"Please enter VolcEngine VOD domain name\",\r\n  \"workflow_node.deploy.form.volcengine_waf_region.label\": \"VolcEngine region\",\r\n  \"workflow_node.deploy.form.volcengine_waf_region.placeholder\": \"Please enter VolcEngine WAF region (e.g. cn-beijing)\",\r\n  \"workflow_node.deploy.form.volcengine_waf_region.tooltip\": \"For more information, see <a href=\\\"https://www.volcengine.com/docs/6511/1594024\\\" target=\\\"_blank\\\">https://www.volcengine.com/docs/6511/1594024</a>\",\r\n  \"workflow_node.deploy.form.volcengine_waf_access_mode.label\": \"VolcEngine WAF access mode\",\r\n  \"workflow_node.deploy.form.volcengine_waf_access_mode.placeholder\": \"Please select VolcEngine WAF access mode\",\r\n  \"workflow_node.deploy.form.volcengine_waf_access_mode.option.cname.label\": \"CNAME access\",\r\n  \"workflow_node.deploy.form.volcengine_waf_domain.label\": \"VolcEngine WAF domain\",\r\n  \"workflow_node.deploy.form.volcengine_waf_domain.placeholder\": \"Please enter VolcEngine WAF domain name\",\r\n  \"workflow_node.deploy.form.wangsu_cdn_domains.label\": \"Wangsu Cloud CDN domains\",\r\n  \"workflow_node.deploy.form.wangsu_cdn_domains.placeholder\": \"Please enter Wangsu Cloud CDN domain names (separated by semicolons)\",\r\n  \"workflow_node.deploy.form.wangsu_cdn_domains.help\": \"Notes: Multiple domains should be separated by semicolons.\",\r\n  \"workflow_node.deploy.form.wangsu_cdn_domains.multiple_input_modal.title\": \"Change Wangsu Cloud CDN domains\",\r\n  \"workflow_node.deploy.form.wangsu_cdn_domains.multiple_input_modal.placeholder\": \"Please enter Wangsu Cloud CDN domain\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_environment.label\": \"Wangsu Cloud environment\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder\": \"Please select Wangsu Cloud environment\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_environment.option.production.label\": \"Production environment\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_environment.option.staging.label\": \"Staging environment\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_domain.label\": \"Wangsu Cloud CDN domain\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder\": \"Please enter Wangsu Cloud CDN domain name\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.label\": \"Wangsu Cloud CDN certificate ID (Optional)\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.placeholder\": \"Please enter Wangsu Cloud CDN certificate ID\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.help\": \"Notes: Leave it blank to import a new certificate; otherwise, to replace the existing one.\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.tooltip\": \"For more information, see <a href=\\\"https://cdnpro.console.wangsu.com/v2/index/#/certificate\\\" target=\\\"_blank\\\">https://cdnpro.console.wangsu.com/v2/index/#/certificate</a>\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.label\": \"Wangsu Cloud CDN Webhook ID (Optional)\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.placeholder\": \"Please enter Wangsu Cloud CDN Webhook ID\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.tooltip\": \"For more information, see <a href=\\\"https://cdnpro.console.wangsu.com/v2/index/#/certificate\\\" target=\\\"_blank\\\">https://cdnpro.console.wangsu.com/v2/index/#/certificate</a>\",\r\n  \"workflow_node.deploy.form.wangsu_certificate_id.label\": \"Wangsu Cloud certificate ID (Optional)\",\r\n  \"workflow_node.deploy.form.wangsu_certificate_id.placeholder\": \"Please enter Wangsu Cloud certificate ID\",\r\n  \"workflow_node.deploy.form.wangsu_certificate_id.help\": \"Notes: Leave it blank to import a new certificate; otherwise, to replace the existing one.\",\r\n  \"workflow_node.deploy.form.wangsu_certificate_id.tooltip\": \"For more information, see <a href=\\\"https://cdn.console.wangsu.com/v2/index#/certificate/list?code=cert_mylist&parentCode=cert_ssl&productCode=certificatemanagement\\\" target=\\\"_blank\\\">https://cdn.console.wangsu.com/v2/index#/certificate/list</a>\",\r\n  \"workflow_node.deploy.form.webhook_data.label\": \"Webhook data (Optional)\",\r\n  \"workflow_node.deploy.form.webhook_data.placeholder\": \"Please enter Webhook data\",\r\n  \"workflow_node.deploy.form.webhook_data.help\": \"Notes: Leave it blank to use the default Webhook data provided by the credential.\",\r\n  \"workflow_node.deploy.form.webhook_data.vartips\": \"Supported variables: <br><ol style=\\\"list-style: disc;\\\"><li><strong>${CERTIMATE_DEPLOYER_COMMONNAME}</strong>: <br>The primary domain or IP address of the certificate.</li><li><strong>${CERTIMATE_DEPLOYER_SUBJECTALTNAMES}</strong>: <br>The domains or IP addresses of the certificate, separated by semicolons.</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE}</strong>: <br>The PEM format content of the certificate file.</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE_SERVER}</strong>: <br>The PEM format content of the server certificate file.</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE_INTERMEDIA}</strong>: <br>The PEM format content of the intermediate CA certificate file.</li><li><strong>${CERTIMATE_DEPLOYER_PRIVATEKEY}</strong>: <br>The PEM format content of the private key file.</li></ol>\",\r\n  \"workflow_node.deploy.form.webhook_timeout.label\": \"Webhook timeout (Optional)\",\r\n  \"workflow_node.deploy.form.webhook_timeout.placeholder\": \"Please enter Webhook timeout\",\r\n  \"workflow_node.deploy.form.webhook_timeout.unit\": \"seconds\",\r\n  \"workflow_node.deploy.form.skip_on_last_succeeded.label\": \"Repeated deployment\",\r\n  \"workflow_node.deploy.form.skip_on_last_succeeded.prefix\": \"If the last deployment was successful, \",\r\n  \"workflow_node.deploy.form.skip_on_last_succeeded.suffix\": \" to re-deploy.\",\r\n  \"workflow_node.deploy.form.skip_on_last_succeeded.switch.on\": \"skip\",\r\n  \"workflow_node.deploy.form.skip_on_last_succeeded.switch.off\": \"not skip\",\r\n\r\n  \"workflow_node.notify.label\": \"Send notification\",\r\n  \"workflow_node.notify.default_name\": \"Notification\",\r\n  \"workflow_node.notify.form_anchor.parameters.tab\": \"Parameters\",\r\n  \"workflow_node.notify.form_anchor.channel.tab\": \"Channel\",\r\n  \"workflow_node.notify.form_anchor.channel.title\": \"Channel settings\",\r\n  \"workflow_node.notify.form_anchor.strategy.tab\": \"Strategy\",\r\n  \"workflow_node.notify.form_anchor.strategy.title\": \"Strategy settings\",\r\n  \"workflow_node.notify.form.subject.label\": \"Subject\",\r\n  \"workflow_node.notify.form.subject.placeholder\": \"Please enter subject\",\r\n  \"workflow_node.notify.form.message.label\": \"Message\",\r\n  \"workflow_node.notify.form.message.placeholder\": \"Please enter message\",\r\n  \"workflow_node.notify.form.template.guide\": \"<details><summary>The content using the \\\"Mustache\\\" syntax (double curly braces) and preceded by \\\"$\\\" in the subject or message are text interpolations. They will be replaced by the actual values. </summary><br>Supported text interpolations: <ol style=\\\"list-style: disc;\\\"><li>Workflow: <ol style=\\\"margin: 0; list-style: '-';\\\"><li><em>workflow.id</em>: The ID of the workflow.</li><li><em>workflow.name</em>: The name of the workflow.</li><li><em>run.id</em>: The ID of the workflow run.</li></ol></li><li>Error: <br><i>(If there are multiple nodes that have failed before this, it always indicate the nearest one.)</i><ol style=\\\"margin: 0; list-style: '-';\\\"><li><em>error.nodeId</em>: The node ID that execution failed.</li><li><em>error.nodeName</em>: The node name that execution failed.</li><li><em>error.message</em>: The error message that execution failed.</li></ol></li><li>Certificate: <br><i>(If there are multiple nodes outputting a certificate before this, it always indicate the nearest one.)</i><ol style=\\\"margin: 0; list-style: '-';\\\"><li><em>certificate.commonName</em>: The primary domain or IP address of the certificate.</li><li><em>certificate.subjectAltNames</em>: The domains or IP addresses of the certificate, separated by semicolons.</li><li><em>certificate.notBefore</em>: The effect time of the certificate, formatted in RFC3339.</li><li><em>certificate.notAfter</em>: The expire time of the certificate, formatted in RFC3339.</li><li><em>certificate.hoursLeft</em>: The left hours of the certificate.</li><li><em>certificate.daysLeft</em>: The left days of the certificate.</li><li><em>certificate.validity</em>: The validity of the certificate.</li></ol></li><li>Other: <ol style=\\\"margin: 0; list-style: '-';\\\"><li><em>now</em>: The current time on the server, formatted in RFC3339. </li></ol></li></ol><br>Example: <br><em>Your workflow {{ $workflow.name }} has failed on node {{ $error.nodeName }} at {{ $now }}.</em></details>\",\r\n  \"workflow_node.notify.form.provider.label\": \"Notification channel\",\r\n  \"workflow_node.notify.form.provider.placeholder\": \"Please select notification channel\",\r\n  \"workflow_node.notify.form.provider.search.placeholder\": \"Search notification channel ...\",\r\n  \"workflow_node.notify.form.provider_access.label\": \"Notification provider credential\",\r\n  \"workflow_node.notify.form.provider_access.placeholder\": \"Please select an credential of notification provider\",\r\n  \"workflow_node.notify.form.provider_access.button\": \"Create\",\r\n  \"workflow_node.notify.form.params_config.label\": \"Parameter settings\",\r\n  \"workflow_node.notify.form.discordbot_channel_id.label\": \"Discord channel ID (Optional)\",\r\n  \"workflow_node.notify.form.discordbot_channel_id.placeholder\": \"Please enter Discord channel ID\",\r\n  \"workflow_node.notify.form.discordbot_channel_id.help\": \"Notes: Leave it blank to use the default channel ID provided by the credential.\",\r\n  \"workflow_node.notify.form.email_format.label\": \"Message format (Optional)\",\r\n  \"workflow_node.notify.form.email_format.placeholder\": \"Please select message format\",\r\n  \"workflow_node.notify.form.email_format.option.plain.label\": \"Plain text\",\r\n  \"workflow_node.notify.form.email_format.option.html.label\": \"HTML\",\r\n  \"workflow_node.notify.form.email_receiver_address.label\": \"Receiver email address (Optional)\",\r\n  \"workflow_node.notify.form.email_receiver_address.placeholder\": \"Please enter receiver email address\",\r\n  \"workflow_node.notify.form.email_receiver_address.help\": \"Notes: Leave it blank to use the default receiver email address provided by the selected credential.\",\r\n  \"workflow_node.notify.form.mattermost_channel_id.label\": \"Mattermost channel ID (Optional)\",\r\n  \"workflow_node.notify.form.mattermost_channel_id.placeholder\": \"Please enter Mattermost channel ID\",\r\n  \"workflow_node.notify.form.mattermost_channel_id.help\": \"Notes: Leave it blank to use the default channel ID provided by the credential.\",\r\n  \"workflow_node.notify.form.slackbot_channel_id.label\": \"Slack channel ID (Optional)\",\r\n  \"workflow_node.notify.form.slackbot_channel_id.placeholder\": \"Please enter Slack channel ID\",\r\n  \"workflow_node.notify.form.slackbot_channel_id.help\": \"Notes: Leave it blank to use the default channel ID provided by the credential.\",\r\n  \"workflow_node.notify.form.telegrambot_chat_id.label\": \"Telegram chat ID (Optional)\",\r\n  \"workflow_node.notify.form.telegrambot_chat_id.placeholder\": \"Please enter Telegram chat ID\",\r\n  \"workflow_node.notify.form.telegrambot_chat_id.help\": \"Notes: Leave it blank to use the default chat ID provided by the selected credential.\",\r\n  \"workflow_node.notify.form.webhook_data.label\": \"Webhook data (Optional)\",\r\n  \"workflow_node.notify.form.webhook_data.placeholder\": \"Please enter Webhook data\",\r\n  \"workflow_node.notify.form.webhook_data.help\": \"Notes: Leave it blank to use the default Webhook data provided by the credential.\",\r\n  \"workflow_node.notify.form.webhook_data.vartips\": \"Supported variables: <br><ol style=\\\"list-style: disc;\\\"><li><strong>${CERTIMATE_NOTIFIER_SUBJECT}</strong>: <br>The subject of notification.</li><li><strong>${CERTIMATE_NOTIFIER_MESSAGE}</strong>: <br>The message of notification.</li></ol>\",\r\n  \"workflow_node.notify.form.webhook_timeout.label\": \"Webhook timeout (Optional)\",\r\n  \"workflow_node.notify.form.webhook_timeout.placeholder\": \"Please enter Webhook timeout\",\r\n  \"workflow_node.notify.form.webhook_timeout.unit\": \"seconds\",\r\n  \"workflow_node.notify.form.skip_on_all_prev_skipped.label\": \"Silent behavior\",\r\n  \"workflow_node.notify.form.skip_on_all_prev_skipped.prefix\": \"If all the previous nodes were skipped, \",\r\n  \"workflow_node.notify.form.skip_on_all_prev_skipped.suffix\": \" to notify.\",\r\n  \"workflow_node.notify.form.skip_on_all_prev_skipped.switch.on\": \"skip\",\r\n  \"workflow_node.notify.form.skip_on_all_prev_skipped.switch.off\": \"not skip\",\r\n\r\n  \"workflow_node.delay.label\": \"Delay\",\r\n  \"workflow_node.delay.default_name\": \"Delay\",\r\n  \"workflow_node.delay.form_anchor.parameters.tab\": \"Parameters\",\r\n  \"workflow_node.delay.form.wait.label\": \"Waiting time\",\r\n  \"workflow_node.delay.form.wait.placeholder\": \"Please enter waiting time\",\r\n  \"workflow_node.delay.form.wait.unit\": \"seconds\",\r\n\r\n  \"workflow_node.condition.label\": \"Parallel/Conditional branch\",\r\n  \"workflow_node.condition.default_name\": \"Parallel\",\r\n  \"workflow_node.condition.default_name.template_certtest_on_expiring_soon\": \"If the certificate will be expiring soon ...\",\r\n  \"workflow_node.condition.default_name.template_certtest_on_expired\": \"If the certificate has expired ...\",\r\n\r\n  \"workflow_node.branch_block.label\": \"Branch\",\r\n  \"workflow_node.branch_block.default_name\": \"Branch\",\r\n  \"workflow_node.branch_block.state.no\": \"Enter Unconditionally\",\r\n  \"workflow_node.branch_block.state.or\": \"Enter when any condition is met\",\r\n  \"workflow_node.branch_block.state.and\": \"Enter when all conditions are met\",\r\n  \"workflow_node.branch_block.form_anchor.parameters.tab\": \"Parameters\",\r\n  \"workflow_node.branch_block.form.expression.label\": \"Conditions to enter the branch\",\r\n  \"workflow_node.branch_block.form.expression.errmsg.invalid\": \"Please enter a valid expression\",\r\n  \"workflow_node.branch_block.form.expression.logical_operator.errmsg\": \"Please select logical operator of conditions\",\r\n  \"workflow_node.branch_block.form.expression.logical_operator.option.and.label\": \"Meeting all of the conditions (AND)\",\r\n  \"workflow_node.branch_block.form.expression.logical_operator.option.or.label\": \"Meeting any of the conditions (OR)\",\r\n  \"workflow_node.branch_block.form.expression.variable.placeholder\": \"Please select\",\r\n  \"workflow_node.branch_block.form.expression.variable.errmsg\": \"Please select variable\",\r\n  \"workflow_node.branch_block.form.expression.operator.placeholder\": \"Please select\",\r\n  \"workflow_node.branch_block.form.expression.operator.errmsg\": \"Please select operator\",\r\n  \"workflow_node.branch_block.form.expression.operator.option.eq.label\": \"equal to\",\r\n  \"workflow_node.branch_block.form.expression.operator.option.eq.alias_is_label\": \"is\",\r\n  \"workflow_node.branch_block.form.expression.operator.option.neq.label\": \"not equal to\",\r\n  \"workflow_node.branch_block.form.expression.operator.option.neq.alias_not_label\": \"is not\",\r\n  \"workflow_node.branch_block.form.expression.operator.option.gt.label\": \"greater than\",\r\n  \"workflow_node.branch_block.form.expression.operator.option.gte.label\": \"greater than or equal to\",\r\n  \"workflow_node.branch_block.form.expression.operator.option.lt.label\": \"less than\",\r\n  \"workflow_node.branch_block.form.expression.operator.option.lte.label\": \"less than or equal to\",\r\n  \"workflow_node.branch_block.form.expression.value.placeholder\": \"Please enter\",\r\n  \"workflow_node.branch_block.form.expression.value.errmsg\": \"Please enter value\",\r\n  \"workflow_node.branch_block.form.expression.value.option.true.label\": \"True\",\r\n  \"workflow_node.branch_block.form.expression.value.option.false.label\": \"False\",\r\n  \"workflow_node.branch_block.form.expression.add_condition.button\": \"Add condition\",\r\n\r\n  \"workflow_node.try_catch.label\": \"Execution result branch\",\r\n  \"workflow_node.try_catch.default_name\": \"Try to ...\",\r\n\r\n  \"workflow_node.catch_block.label\": \"Execution failure branch\",\r\n  \"workflow_node.catch_block.default_name\": \"On failed ...\",\r\n\r\n  \"workflow_node.end.label\": \"End\",\r\n  \"workflow_node.end.default_name\": \"End\"\r\n}\r\n"
  },
  {
    "path": "ui/src/i18n/locales/en/nls.workflow.runs.json",
    "content": "{\n  \"workflow_run.action.view.menu\": \"View details\",\n  \"workflow_run.action.cancel.menu\": \"Cancel\",\n  \"workflow_run.action.cancel.modal.title\": \"Cancel workflow run\",\n  \"workflow_run.action.cancel.modal.content\": \"Are you sure to cancel this run?\",\n  \"workflow_run.action.delete.menu\": \"Delete\",\n  \"workflow_run.action.delete.modal.title\": \"Delete \\\"{{name}}\\\"\",\n  \"workflow_run.action.delete.modal.content\": \"Are you sure want to delete this workflow run? <br>This action cannot be undone.\",\n  \"workflow_run.action.batch_delete.modal.title\": \"Delete workflow runs\",\n  \"workflow_run.action.batch_delete.modal.content\": \"Are you sure want to delete these {{count}} selected workflow runs? <br>This action cannot be undone.\",\n\n  \"workflow_run.deletion.alert\": \"The workflow run contains the execution results of each node. Deleting it may trigger re-application or re-deployment of certificates due to the inability to find the previous execution result. Please do not delete unless necessary. Recommend keeping it for at least 180 days. \",\n  \"workflow_run.cancellation.alert\": \"If the process is unexpectedly terminated or the server times out, you can manually cancel long-hanging runs to prevent blocking subsequent executions.\",\n\n  \"workflow_run.nodata.title\": \"This workflow has no runs yet\",\n  \"workflow_run.nodata.description\": \"It looks like you don't have any runs. Get started by running this workflow.\",\n\n  \"workflow_run.props.workflow\": \"Workflow\",\n  \"workflow_run.props.status\": \"Status\",\n  \"workflow_run.props.status.pending\": \"Pending\",\n  \"workflow_run.props.status.processing\": \"Processing\",\n  \"workflow_run.props.status.succeeded\": \"Succeeded\",\n  \"workflow_run.props.status.failed\": \"Failed\",\n  \"workflow_run.props.status.canceled\": \"Canceled\",\n  \"workflow_run.props.trigger\": \"Trigger\",\n  \"workflow_run.props.trigger.scheduled\": \"Scheduled\",\n  \"workflow_run.props.trigger.manual\": \"Manual\",\n  \"workflow_run.props.started_at\": \"Started at\",\n  \"workflow_run.props.ended_at\": \"Ended at\",\n  \"workflow_run.props.artifacts\": \"Artifacts\",\n\n  \"workflow_run.base.description\": \"Triggered {{trigger}} at {{startedAt}}\",\n  \"workflow_run.base.description_with_time_cost\": \"Triggered {{trigger}} at {{startedAt}}. Time cost: {{timeCost}}.\",\n  \"workflow_run.base.trigger.scheduled\": \"scheduledly\",\n  \"workflow_run.base.trigger.manual\": \"manually\",\n\n  \"workflow_run.process\": \"Process\",\n  \"workflow_run.process.menu.export\": \"Export\",\n\n  \"workflow_run.logs\": \"Logs\",\n  \"workflow_run.logs.menu.show_timestamps\": \"Show timestamps\",\n  \"workflow_run.logs.menu.show_whitespaces\": \"Show whitespaces\",\n  \"workflow_run.logs.menu.download_logs\": \"Download logs\",\n\n  \"workflow_run.artifacts\": \"Artifacts\",\n  \"workflow_run_artifact.props.type\": \"Type\",\n  \"workflow_run_artifact.props.type.certificate\": \"Certificate\",\n  \"workflow_run_artifact.props.name\": \"Name\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/en/nls.workflow.vars.json",
    "content": "{\n  \"workflow.variables.type.certificate.label\": \"Certificate\",\n\n  \"workflow.variables.selector.hours_left.label\": \"Hours left\",\n  \"workflow.variables.selector.days_left.label\": \"Days left\",\n  \"workflow.variables.selector.validity.label\": \"Validity\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/index.ts",
    "content": "import { type Resource } from \"i18next\";\n\nimport en from \"./en\";\nimport zh from \"./zh\";\n\nexport const LOCALE_ZH_NAME = \"zh\" as const;\nexport const LOCALE_EN_NAME = \"en\" as const;\n\nconst resources: Resource = {\n  [LOCALE_ZH_NAME]: {\n    name: \"简体中文\",\n    translation: zh,\n  },\n  [LOCALE_EN_NAME]: {\n    name: \"English\",\n    translation: en,\n  },\n};\n\nexport default resources;\n"
  },
  {
    "path": "ui/src/i18n/locales/zh/index.ts",
    "content": "﻿import nlsAccess from \"./nls.access.json\";\nimport nlsCertificate from \"./nls.certificate.json\";\nimport nlsCommon from \"./nls.common.json\";\nimport nlsDashboard from \"./nls.dashboard.json\";\nimport nlsLogin from \"./nls.login.json\";\nimport nlsPreset from \"./nls.preset.json\";\nimport nlsProvider from \"./nls.provider.json\";\nimport nlsSettings from \"./nls.settings.json\";\nimport nlsWorkflow from \"./nls.workflow.json\";\nimport nlsWorkflowNodes from \"./nls.workflow.nodes.json\";\nimport nlsWorkflowRuns from \"./nls.workflow.runs.json\";\nimport nlsWorkflowVars from \"./nls.workflow.vars.json\";\n\nexport default Object.freeze({\n  ...nlsCommon,\n  ...nlsLogin,\n  ...nlsDashboard,\n  ...nlsSettings,\n  ...nlsProvider,\n  ...nlsAccess,\n  ...nlsPreset,\n  ...nlsCertificate,\n  ...nlsWorkflow,\n  ...nlsWorkflowNodes,\n  ...nlsWorkflowRuns,\n  ...nlsWorkflowVars,\n});\n"
  },
  {
    "path": "ui/src/i18n/locales/zh/nls.access.json",
    "content": "﻿{\r\n  \"access.page.title\": \"授权凭据\",\r\n  \"access.page.subtitle\": \"授权凭据中存储有用于访问特定第三方应用程序或服务的身份验证信息（如账号密码、接口密钥、API 令牌等）。\",\r\n\r\n  \"access.nodata.title\": \"暂无授权\",\r\n  \"access.nodata.description\": \"当前未找到授权信息。请先创建。\",\r\n  \"access.nodata.button\": \"新建授权\",\r\n\r\n  \"access.search.placeholder\": \"按授权名称搜索……\",\r\n\r\n  \"access.action.create.button\": \"新建授权\",\r\n  \"access.action.create.modal.title\": \"新建授权\",\r\n  \"access.action.modify.menu\": \"编辑授权\",\r\n  \"access.action.modify.modal.title\": \"编辑授权\",\r\n  \"access.action.duplicate.menu\": \"复制授权\",\r\n  \"access.action.duplicate.modal.title\": \"复制授权\",\r\n  \"access.action.delete.menu\": \"删除授权\",\r\n  \"access.action.delete.modal.title\": \"删除「{{name}}」\",\r\n  \"access.action.delete.modal.content\": \"确定要删除该授权吗？<br>注意此操作不可撤销，请谨慎操作。\",\r\n  \"access.action.batch_delete.modal.title\": \"删除授权\",\r\n  \"access.action.batch_delete.modal.content\": \"确定要删除这 {{count}} 个被选中的授权吗？<br>注意此操作不可撤销，请谨慎操作。\",\r\n  \"access.action.test_notify.button\": \"测试通知\",\r\n\r\n  \"access.props.name\": \"名称\",\r\n  \"access.props.provider.usage.dns\": \"DNS 提供商\",\r\n  \"access.props.provider.usage.hosting\": \"主机提供商\",\r\n  \"access.props.provider.usage.ca\": \"证书颁发机构\",\r\n  \"access.props.provider.usage.notification\": \"通知渠道\",\r\n  \"access.props.provider.builtin\": \"内置\",\r\n  \"access.props.usage.dns_hosting\": \"提供商\",\r\n  \"access.props.usage.ca\": \"证书颁发机构\",\r\n  \"access.props.usage.notification\": \"通知渠道\",\r\n  \"access.props.created_at\": \"创建时间\",\r\n  \"access.props.updated_at\": \"更新时间\",\r\n\r\n  \"access.new.title\": \"新建授权\",\r\n  \"access.new.subtitle\": \"使用此授权访问特定的第三方应用程序或服务。\",\r\n\r\n  \"access.form.name.label\": \"名称\",\r\n  \"access.form.name.placeholder\": \"请输入授权名称\",\r\n  \"access.form.provider.label\": \"提供商\",\r\n  \"access.form.provider.placeholder\": \"请选择提供商\",\r\n  \"access.form.provider.help\": \"提供商分为两种类型：<br>【DNS 提供商】你的 DNS 托管方，通常等同于域名注册商，用于在申请证书时管理域名解析记录。<br>【主机提供商】你的服务器或云服务的托管方，用于部署签发的证书。\",\r\n  \"access.form.provider.search.placeholder\": \"搜索提供商……\",\r\n  \"access.form.shared_acme_eab_kid.label\": \"ACME EAB KID\",\r\n  \"access.form.shared_acme_eab_kid.placeholder\": \"请输入 ACME EAB KID\",\r\n  \"access.form.shared_acme_eab_hmac_key.label\": \"ACME EAB HMAC Key\",\r\n  \"access.form.shared_acme_eab_hmac_key.placeholder\": \"请输入 ACME EAB HMAC Key\",\r\n  \"access.form.shared_allow_insecure_conns.label\": \"忽略 SSL/TLS 证书错误\",\r\n  \"access.form.1panel_server_url.label\": \"1Panel 服务地址\",\r\n  \"access.form.1panel_server_url.placeholder\": \"请输入 1Panel 服务地址\",\r\n  \"access.form.1panel_server_url.help\": \"提示：请勿包含安全入口后缀。\",\r\n  \"access.form.1panel_api_version.label\": \"1Panel 版本\",\r\n  \"access.form.1panel_api_version.placeholder\": \"请选择 1Panel 版本\",\r\n  \"access.form.1panel_api_key.label\": \"1Panel 接口密钥\",\r\n  \"access.form.1panel_api_key.placeholder\": \"请输入 1Panel 接口密钥\",\r\n  \"access.form.1panel_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://1panel.cn/docs/dev_manual/api_manual/\\\" target=\\\"_blank\\\">https://1panel.cn/docs/dev_manual/api_manual/</a>\",\r\n  \"access.form.35cn_username.label\": \"三五互联代理商用户名\",\r\n  \"access.form.35cn_username.placeholder\": \"请输入三五互联代理商用户名\",\r\n  \"access.form.35cn_api_password.label\": \"三五互联代理商 API 密码\",\r\n  \"access.form.35cn_api_password.placeholder\": \"请输入三五互联代理商 API 密码\",\r\n  \"access.form.35cn_agent.guide\": \"三五互联 API 仅支持代理商调用。点击下方链接了解更多：<br><a href=\\\"https://console-docs.apipost.cn/preview/ab2c3103b22855ba/fac91d1e43fafb69?target_id=fa930623-3109-489d-9835-75fdfba07fbb\\\" target=\\\"_blank\\\">https://www.35.com/agent/mode-api.asp</a>\",\r\n  \"access.form.51dnscom_api_key.label\": \"帝恩思 API Key\",\r\n  \"access.form.51dnscom_api_key.placeholder\": \"请输入帝恩思 API Key\",\r\n  \"access.form.51dnscom_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.51dns.com/member/apiSet\\\" target=\\\"_blank\\\">https://www.51dns.com/member/apiSet</a>\",\r\n  \"access.form.51dnscom_api_secret.label\": \"帝恩思 API Secret\",\r\n  \"access.form.51dnscom_api_secret.placeholder\": \"请输入帝恩思 API Secret\",\r\n  \"access.form.51dnscom_api_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.51dns.com/member/apiSet\\\" target=\\\"_blank\\\">https://www.51dns.com/member/apiSet</a>\",\r\n  \"access.form.acmeca_endpoint.label\": \"服务端点\",\r\n  \"access.form.acmeca_endpoint.placeholder\": \"请输入服务端点\",\r\n  \"access.form.acmeca_endpoint.tooltip\": \"这是什么？请参阅 <a href=\\\"https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1\\\" target=\\\"_blank\\\">https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1</a>\",\r\n  \"access.form.acmeca_eab_kid.label\": \"ACME EAB KID（可选）\",\r\n  \"access.form.acmeca_eab_kid.placeholder\": \"请输入 ACME EAB KID\",\r\n  \"access.form.acmeca_eab_hmac_key.label\": \"ACME EAB HMAC Key（可选）\",\r\n  \"access.form.acmeca_eab_hmac_key.placeholder\": \"请输入 ACME EAB HMAC Key\",\r\n  \"access.form.acmedns_server_url.label\": \"ACME-DNS 服务地址\",\r\n  \"access.form.acmedns_server_url.placeholder\": \"请输入 ACME-DNS 服务地址\",\r\n  \"access.form.acmedns_credentials.label\": \"ACME-DNS 凭证文件\",\r\n  \"access.form.acmedns_credentials.placeholder\": \"请输入 ACME-DNS 凭证文件\",\r\n  \"access.form.acmedns_credentials.tooltip\": \"这是什么？请参阅 <a href=\\\"https://github.com/joohoi/acme-dns\\\" target=\\\"_blank\\\">https://github.com/joohoi/acme-dns</a>\",\r\n  \"access.form.acmehttpreq_endpoint.label\": \"服务端点\",\r\n  \"access.form.acmehttpreq_endpoint.placeholder\": \"请输入服务端点\",\r\n  \"access.form.acmehttpreq_endpoint.tooltip\": \"这是什么？请参阅 <a href=\\\"https://go-acme.github.io/lego/dns/httpreq/\\\" target=\\\"_blank\\\">https://go-acme.github.io/lego/dns/httpreq/</a>\",\r\n  \"access.form.acmehttpreq_mode.label\": \"模式\",\r\n  \"access.form.acmehttpreq_mode.placeholder\": \"请选择模式\",\r\n  \"access.form.acmehttpreq_mode.tooltip\": \"这是什么？请参阅 <a href=\\\"https://go-acme.github.io/lego/dns/httpreq/\\\" target=\\\"_blank\\\">https://go-acme.github.io/lego/dns/httpreq/</a>\",\r\n  \"access.form.acmehttpreq_username.label\": \"HTTP 基本认证用户名（可选）\",\r\n  \"access.form.acmehttpreq_username.placeholder\": \"请输入 HTTP 基本认证用户名\",\r\n  \"access.form.acmehttpreq_username.tooltip\": \"这是什么？请参阅 <a href=\\\"https://go-acme.github.io/lego/dns/httpreq/\\\" target=\\\"_blank\\\">https://go-acme.github.io/lego/dns/httpreq/</a>\",\r\n  \"access.form.acmehttpreq_password.label\": \"HTTP 基本认证密码（可选）\",\r\n  \"access.form.acmehttpreq_password.placeholder\": \"请输入 HTTP 基本认证密码\",\r\n  \"access.form.acmehttpreq_password.tooltip\": \"这是什么？请参阅 <a href=\\\"https://go-acme.github.io/lego/dns/httpreq/\\\" target=\\\"_blank\\\">https://go-acme.github.io/lego/dns/httpreq/</a>\",\r\n  \"access.form.actalisssl_eab.guide\": \"点击下方链接了解如何获取 Actalis SSL EAB：<br><a href=\\\"https://www.actalis.com/manage-with-acme\\\" target=\\\"_blank\\\">https://www.actalis.com/manage-with-acme</a>\",\r\n  \"access.form.akamai_host.label\": \"Akamai API Host\",\r\n  \"access.form.akamai_host.placeholder\": \"请输入 Akamai API Host\",\r\n  \"access.form.akamai_host.tooltip\": \"这是什么？请参阅 <a href=\\\"https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials\\\" target=\\\"_blank\\\">https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials</a>\",\r\n  \"access.form.akamai_client_token.label\": \"Akamai ClientToken\",\r\n  \"access.form.akamai_client_token.placeholder\": \"请输入 Akamai ClientToken\",\r\n  \"access.form.akamai_client_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials\\\" target=\\\"_blank\\\">https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials</a>\",\r\n  \"access.form.akamai_client_secret.label\": \"Akamai ClientSecret\",\r\n  \"access.form.akamai_client_secret.placeholder\": \"请输入 Akamai ClientSecret\",\r\n  \"access.form.akamai_client_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials\\\" target=\\\"_blank\\\">https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials</a>\",\r\n  \"access.form.akamai_access_token.label\": \"Akamai AccessToken\",\r\n  \"access.form.akamai_access_token.placeholder\": \"请输入 Akamai AccessToken\",\r\n  \"access.form.akamai_access_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials\\\" target=\\\"_blank\\\">https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials</a>\",\r\n  \"access.form.aliyun_access_key_id.label\": \"阿里云 AccessKeyID\",\r\n  \"access.form.aliyun_access_key_id.placeholder\": \"请输入阿里云 AccessKeyID\",\r\n  \"access.form.aliyun_access_key_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/ram/user-guide/create-an-accesskey-pair\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/ram/user-guide/create-an-accesskey-pair</a>\",\r\n  \"access.form.aliyun_access_key_secret.label\": \"阿里云 AccessKeySecret\",\r\n  \"access.form.aliyun_access_key_secret.placeholder\": \"请输入阿里云 AccessKeySecret\",\r\n  \"access.form.aliyun_access_key_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/ram/user-guide/create-an-accesskey-pair\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/ram/user-guide/create-an-accesskey-pair</a>\",\r\n  \"access.form.aliyun_resource_group_id.label\": \"阿里云资源组 ID（可选）\",\r\n  \"access.form.aliyun_resource_group_id.placeholder\": \"请输入阿里云资源组 ID\",\r\n  \"access.form.aliyun_resource_group_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/resource-management/resource-group/product-overview\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/resource-management/resource-group/product-overview</a>\",\r\n  \"access.form.apisix_server_url.label\": \"APISIX 服务地址\",\r\n  \"access.form.apisix_server_url.placeholder\": \"请输入 APISIX 服务地址\",\r\n  \"access.form.apisix_api_key.label\": \"APISIX Admin API Key\",\r\n  \"access.form.apisix_api_key.placeholder\": \"请输入 APISIX Admin API Key\",\r\n  \"access.form.apisix_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://apisix.apache.org/zh/docs/apisix/admin-api/\\\" target=\\\"_blank\\\">https://apisix.apache.org/zh/docs/apisix/admin-api/</a>\",\r\n  \"access.form.arvancloud_api_key.label\": \"ArvanCloud API 密钥\",\r\n  \"access.form.arvancloud_api_key.placeholder\": \"请输入 ArvanCloud API 密钥\",\r\n  \"access.form.arvancloud_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.arvancloud.ir/en/developer-tools/api/api-key\\\" target=\\\"_blank\\\">https://docs.arvancloud.ir/en/developer-tools/api/api-key</a>\",\r\n  \"access.form.aws_access_key_id.label\": \"AWS AccessKeyID\",\r\n  \"access.form.aws_access_key_id.placeholder\": \"请输入 AWS AccessKeyID\",\r\n  \"access.form.aws_access_key_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/id_credentials_access-keys.html\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/id_credentials_access-keys.html</a>\",\r\n  \"access.form.aws_secret_access_key.label\": \"AWS SecretAccessKey\",\r\n  \"access.form.aws_secret_access_key.placeholder\": \"请输入 AWS SecretAccessKey\",\r\n  \"access.form.aws_secret_access_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/id_credentials_access-keys.html\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/id_credentials_access-keys.html</a>\",\r\n  \"access.form.azure_tenant_id.label\": \"Azure 租户 ID\",\r\n  \"access.form.azure_tenant_id.placeholder\": \"请输入 Azure 租户 ID\",\r\n  \"access.form.azure_tenant_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://learn.microsoft.com/zh-cn/azure/azure-portal/get-subscription-tenant-id\\\" target=\\\"_blank\\\">https://learn.microsoft.com/zh-cn/azure/azure-portal/get-subscription-tenant-id</a>\",\r\n  \"access.form.azure_client_id.label\": \"Azure 客户端 ID\",\r\n  \"access.form.azure_client_id.placeholder\": \"请输入 Azure 客户端 ID\",\r\n  \"access.form.azure_client_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://learn.microsoft.com/zh-cn/azure/azure-monitor/logs/api/register-app-for-token\\\" target=\\\"_blank\\\">https://learn.microsoft.com/zh-cn/azure/azure-monitor/logs/api/register-app-for-token</a>\",\r\n  \"access.form.azure_client_secret.label\": \"Azure 客户端密码\",\r\n  \"access.form.azure_client_secret.placeholder\": \"请输入 Azure 客户端密码\",\r\n  \"access.form.azure_client_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://learn.microsoft.com/zh-cn/azure/azure-monitor/logs/api/register-app-for-token\\\" target=\\\"_blank\\\">https://learn.microsoft.com/zh-cn/azure/azure-monitor/logs/api/register-app-for-token</a>\",\r\n  \"access.form.azure_subscription_id.label\": \"Azure 订阅 ID（可选）\",\r\n  \"access.form.azure_subscription_id.placeholder\": \"请输入 Azure 订阅 ID\",\r\n  \"access.form.azure_subscription_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://learn.microsoft.com/zh-cn/azure/azure-portal/get-subscription-tenant-id\\\" target=\\\"_blank\\\">https://learn.microsoft.com/zh-cn/azure/azure-portal/get-subscription-tenant-id</a>\",\r\n  \"access.form.azure_resource_group_name.label\": \"Azure 资源组名称（可选）\",\r\n  \"access.form.azure_resource_group_name.placeholder\": \"请输入 Azure 资源组名称\",\r\n  \"access.form.azure_resource_group_name.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.azure.cn/zh-cn/azure-resource-manager/management/manage-resource-groups-portal\\\" target=\\\"_blank\\\">https://docs.azure.cn/zh-cn/azure-resource-manager/management/manage-resource-groups-portal</a>\",\r\n  \"access.form.azure_cloud_name.label\": \"Azure 主权云环境（可选）\",\r\n  \"access.form.azure_cloud_name.placeholder\": \"请输入 Azure 主权云环境（例如：public）\",\r\n  \"access.form.azure_cloud_name.tooltip\": \"这是什么？请参阅 <a href=\\\"https://learn.microsoft.com/zh-cn/azure/developer/azure-developer-cli/sovereign-clouds\\\" target=\\\"_blank\\\">https://learn.microsoft.com/zh-cn/azure/developer/azure-developer-cli/sovereign-clouds</a>\",\r\n  \"access.form.baiducloud_access_key_id.label\": \"百度智能云 AccessKeyID\",\r\n  \"access.form.baiducloud_access_key_id.placeholder\": \"请输入百度智能云 AccessKeyID\",\r\n  \"access.form.baiducloud_access_key_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.baidu.com/doc/Reference/s/jjwvz2e3p\\\" target=\\\"_blank\\\">https://cloud.baidu.com/doc/Reference/s/jjwvz2e3p</a>\",\r\n  \"access.form.baiducloud_secret_access_key.label\": \"百度智能云 SecretAccessKey\",\r\n  \"access.form.baiducloud_secret_access_key.placeholder\": \"请输入百度智能云 SecretAccessKey\",\r\n  \"access.form.baiducloud_secret_access_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.baidu.com/doc/Reference/s/jjwvz2e3p\\\" target=\\\"_blank\\\">https://cloud.baidu.com/doc/Reference/s/jjwvz2e3p</a>\",\r\n  \"access.form.baishan_api_token.label\": \"白山云 API Token\",\r\n  \"access.form.baishan_api_token.placeholder\": \"请输入白山云 API Token\",\r\n  \"access.form.baotapanel_server_url.label\": \"宝塔面板服务地址\",\r\n  \"access.form.baotapanel_server_url.placeholder\": \"请输入宝塔面板服务地址\",\r\n  \"access.form.baotapanel_server_url.help\": \"提示：请勿包含安全入口后缀。\",\r\n  \"access.form.baotapanel_api_key.label\": \"宝塔面板接口密钥\",\r\n  \"access.form.baotapanel_api_key.placeholder\": \"请输入宝塔面板接口密钥\",\r\n  \"access.form.baotapanel_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.bt.cn/bbs/thread-113890-1-1.html\\\" target=\\\"_blank\\\">https://www.bt.cn/bbs/thread-113890-1-1.html</a>\",\r\n  \"access.form.baotapanelgo_server_url.label\": \"宝塔面板极速版服务地址\",\r\n  \"access.form.baotapanelgo_server_url.placeholder\": \"请输入宝塔面板极速版服务地址\",\r\n  \"access.form.baotapanelgo_server_url.help\": \"提示：请勿包含安全入口后缀。\",\r\n  \"access.form.baotapanelgo_api_key.label\": \"宝塔面板极速版接口密钥\",\r\n  \"access.form.baotapanelgo_api_key.placeholder\": \"请输入宝塔面板极速版接口密钥\",\r\n  \"access.form.baotapanelgo_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.bt.cn/bbs/thread-113890-1-1.html\\\" target=\\\"_blank\\\">https://www.bt.cn/bbs/thread-113890-1-1.html</a>\",\r\n  \"access.form.baotawaf_server_url.label\": \"堡塔云 WAF 服务地址\",\r\n  \"access.form.baotawaf_server_url.placeholder\": \"请输入堡塔云 WAF 服务地址\",\r\n  \"access.form.baotawaf_server_url.help\": \"提示：请勿包含安全入口后缀。\",\r\n  \"access.form.baotawaf_api_key.label\": \"堡塔云 WAF 接口密钥\",\r\n  \"access.form.baotawaf_api_key.placeholder\": \"请输入堡塔云 WAF 接口密钥\",\r\n  \"access.form.baotawaf_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://github.com/aaPanel/aaWAF/blob/main/API.md\\\" target=\\\"_blank\\\">https://github.com/aaPanel/aaWAF/blob/main/API.md</a>\",\r\n  \"access.form.bookmyname_username.label\": \"BookMyName 用户名\",\r\n  \"access.form.bookmyname_username.placeholder\": \"请输入 BookMyName 用户名\",\r\n  \"access.form.bookmyname_password.label\": \"BookMyName 用户密码\",\r\n  \"access.form.bookmyname_password.placeholder\": \"请输入 BookMyName 用户密码\",\r\n  \"access.form.bunny_api_key.label\": \"Bunny API Key\",\r\n  \"access.form.bunny_api_key.placeholder\": \"请输入 Bunny API Key\",\r\n  \"access.form.bunny_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.bunny.net/reference/bunnynet-api-overview\\\" target=\\\"_blank\\\">https://docs.bunny.net/reference/bunnynet-api-overview</a>\",\r\n  \"access.form.byteplus_access_key.label\": \"BytePlus AccessKey\",\r\n  \"access.form.byteplus_access_key.placeholder\": \"请输入 BytePlus AccessKey\",\r\n  \"access.form.byteplus_access_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.byteplus.com/zh-CN/docs/byteplus-platform/docs-managing-keys\\\" target=\\\"_blank\\\">https://docs.byteplus.com/zh-CN/docs/byteplus-platform/docs-managing-keys</a>\",\r\n  \"access.form.byteplus_secret_key.label\": \"BytePlus SecretKey\",\r\n  \"access.form.byteplus_secret_key.placeholder\": \"请输入 BytePlus SecretKey\",\r\n  \"access.form.byteplus_secret_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.byteplus.com/zh-CN/docs/byteplus-platform/docs-managing-keys\\\" target=\\\"_blank\\\">https://docs.byteplus.com/zh-CN/docs/byteplus-platform/docs-managing-keys</a>\",\r\n  \"access.form.cachefly_api_token.label\": \"CacheFly API Token\",\r\n  \"access.form.cachefly_api_token.placeholder\": \"请输入 CacheFly API Token\",\r\n  \"access.form.cachefly_api_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://kb.cachefly.com/kb/guide/en/generating-tokens-and-keys-Oll9Irt5TI/Steps/2460228\\\" target=\\\"_blank\\\">https://kb.cachefly.com/kb/guide/en/generating-tokens-and-keys-Oll9Irt5TI/Steps/2460228</a>\",\r\n  \"access.form.cdnfly_server_url.label\": \"Cdnfly 服务地址\",\r\n  \"access.form.cdnfly_server_url.placeholder\": \"请输入 Cdnfly 服务地址\",\r\n  \"access.form.cdnfly_api_key.label\": \"Cdnfly 用户端 API Key\",\r\n  \"access.form.cdnfly_api_key.placeholder\": \"请输入 Cdnfly 用户端 API Key\",\r\n  \"access.form.cdnfly_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://doc.cdnfly.cn/shiyongjieshao.html\\\" target=\\\"_blank\\\">https://doc.cdnfly.cn/shiyongjieshao.html</a>\",\r\n  \"access.form.cdnfly_api_secret.label\": \"Cdnfly 用户端 API Secret\",\r\n  \"access.form.cdnfly_api_secret.placeholder\": \"请输入 Cdnfly 用户端 API Secret\",\r\n  \"access.form.cdnfly_api_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://doc.cdnfly.cn/shiyongjieshao.html\\\" target=\\\"_blank\\\">https://doc.cdnfly.cn/shiyongjieshao.html</a>\",\r\n  \"access.form.cloudflare_dns_api_token.label\": \"Cloudflare DNS API 令牌\",\r\n  \"access.form.cloudflare_dns_api_token.placeholder\": \"请输入 Cloudflare DNS API 令牌\",\r\n  \"access.form.cloudflare_dns_api_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://developers.cloudflare.com/fundamentals/api/get-started/create-token/\\\" target=\\\"_blank\\\">https://developers.cloudflare.com/fundamentals/api/get-started/create-token/</a>\",\r\n  \"access.form.cloudflare_zone_api_token.label\": \"Cloudflare Zone API 令牌（可选）\",\r\n  \"access.form.cloudflare_zone_api_token.placeholder\": \"请输入 Cloudflare Zone API 令牌\",\r\n  \"access.form.cloudflare_zone_api_token.help\": \"提示：仅当你将 DNS API 令牌范围指定为<b>特定域</b>时需要填写，请将 Zone API 令牌范围指定为<b>全部域</b>，并分配 <i>Zone/Zone/Read</i> 权限。\",\r\n  \"access.form.cloudflare_zone_api_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://developers.cloudflare.com/fundamentals/api/get-started/create-token/\\\" target=\\\"_blank\\\">https://developers.cloudflare.com/fundamentals/api/get-started/create-token/</a>\",\r\n  \"access.form.cloudns_auth_id.label\": \"ClouDNS API 用户 ID\",\r\n  \"access.form.cloudns_auth_id.placeholder\": \"请输入 ClouDNS API 用户 ID\",\r\n  \"access.form.cloudns_auth_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.cloudns.net/wiki/article/42/\\\" target=\\\"_blank\\\">https://www.cloudns.net/wiki/article/42/</a>\",\r\n  \"access.form.cloudns_auth_password.label\": \"ClouDNS API 用户密码\",\r\n  \"access.form.cloudns_auth_password.placeholder\": \"请输入 ClouDNS API 用户密码\",\r\n  \"access.form.cloudns_auth_password.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.cloudns.net/wiki/article/42/\\\" target=\\\"_blank\\\">https://www.cloudns.net/wiki/article/42/</a>\",\r\n  \"access.form.cmcccloud_access_key_id.label\": \"移动云 AccessKeyID\",\r\n  \"access.form.cmcccloud_access_key_id.placeholder\": \"请输入移动云 AccessKeyID\",\r\n  \"access.form.cmcccloud_access_key_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://ecloud.10086.cn/op-help-center/doc/article/49739\\\" target=\\\"_blank\\\">https://ecloud.10086.cn/op-help-center/doc/article/49739</a>\",\r\n  \"access.form.cmcccloud_access_key_secret.label\": \"移动云 AccessKeySecret\",\r\n  \"access.form.cmcccloud_access_key_secret.placeholder\": \"请输入移动云 AccessKeySecret\",\r\n  \"access.form.cmcccloud_access_key_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://ecloud.10086.cn/op-help-center/doc/article/49739\\\" target=\\\"_blank\\\">https://ecloud.10086.cn/op-help-center/doc/article/49739</a>\",\r\n  \"access.form.constellix_api_key.label\": \"Constellix API Key\",\r\n  \"access.form.constellix_api_key.placeholder\": \"请输入 Constellix API Key\",\r\n  \"access.form.constellix_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://support.constellix.com/hc/en-us/articles/34574197390491-How-to-Generate-an-API-Key\\\" target=\\\"_blank\\\">https://support.constellix.com/hc/en-us/articles/34574197390491-How-to-Generate-an-API-Key</a>\",\r\n  \"access.form.constellix_secret_key.label\": \"Constellix Secret Key\",\r\n  \"access.form.constellix_secret_key.placeholder\": \"请输入 Constellix Secret Key\",\r\n  \"access.form.constellix_secret_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://support.constellix.com/hc/en-us/articles/34574197390491-How-to-Generate-an-API-Key\\\" target=\\\"_blank\\\">https://support.constellix.com/hc/en-us/articles/34574197390491-How-to-Generate-an-API-Key</a>\",\r\n  \"access.form.cpanel_server_url.label\": \"cPanel 服务地址\",\r\n  \"access.form.cpanel_server_url.placeholder\": \"请输入 cPanel 服务地址\",\r\n  \"access.form.cpanel_username.label\": \"cPanel 用户名\",\r\n  \"access.form.cpanel_username.placeholder\": \"请输入 cPanel 用户名\",\r\n  \"access.form.cpanel_api_token.label\": \"cPanel API Token\",\r\n  \"access.form.cpanel_api_token.placeholder\": \"请输入 cPanel API Token\",\r\n  \"access.form.cpanel_api_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.cpanel.net/cpanel/security/manage-api-tokens-in-cpanel/\\\" target=\\\"_blank\\\">https://docs.cpanel.net/cpanel/security/manage-api-tokens-in-cpanel/</a>\",\r\n  \"access.form.ctcccloud_access_key_id.label\": \"天翼云 AccessKeyID\",\r\n  \"access.form.ctcccloud_access_key_id.placeholder\": \"请输入天翼云 AccessKeyID\",\r\n  \"access.form.ctcccloud_access_key_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.ctyun.cn/document/10015882/10015953\\\" target=\\\"_blank\\\">https://www.ctyun.cn/document/10015882/10015953</a>\",\r\n  \"access.form.ctcccloud_secret_access_key.label\": \"天翼云 SecretAccessKey\",\r\n  \"access.form.ctcccloud_secret_access_key.placeholder\": \"请输入天翼云 SecretAccessKey\",\r\n  \"access.form.ctcccloud_secret_access_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.ctyun.cn/document/10015882/10015953\\\" target=\\\"_blank\\\">https://www.ctyun.cn/document/10015882/10015953</a>\",\r\n  \"access.form.desec_token.label\": \"deSEC Token\",\r\n  \"access.form.desec_token.placeholder\": \"请输入 deSEC Token\",\r\n  \"access.form.desec_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://desec.readthedocs.io/en/latest/auth/tokens.html#manage-tokens\\\" target=\\\"_blank\\\">https://desec.readthedocs.io/en/latest/auth/tokens.html</a>\",\r\n  \"access.form.digicert_eab.guide\": \"点击下方链接了解如何获取 DigiCert EAB：<br><a href=\\\"https://docs.digicert.com/zh/certcentral/certificate-tools/certificate-lifecycle-automation-guides/third-party-acme-integration/use-legacy-certcentral-acme-credentials.html\\\" target=\\\"_blank\\\">https://docs.digicert.com/zh/certcentral/certificate-tools/certificate-lifecycle-automation-guides/third-party-acme-integration/use-legacy-certcentral-acme-credentials.html</a>\",\r\n  \"access.form.digitalocean_access_token.label\": \"DigitalOcean Access Token\",\r\n  \"access.form.digitalocean_access_token.placeholder\": \"请输入 DigitalOcean Access Token\",\r\n  \"access.form.digitalocean_access_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.digitalocean.com/reference/api/create-personal-access-token/\\\" target=\\\"_blank\\\">https://docs.digitalocean.com/reference/api/create-personal-access-token/</a>\",\r\n  \"access.form.dingtalkbot_webhook_url.label\": \"钉钉群机器人 Webhook 地址\",\r\n  \"access.form.dingtalkbot_webhook_url.placeholder\": \"请输入钉钉群机器人 Webhook 地址\",\r\n  \"access.form.dingtalkbot_webhook_url.tooltip\": \"这是什么？请参阅 <a href=\\\"https://open.dingtalk.com/document/orgapp/obtain-the-webhook-address-of-a-custom-robot\\\" target=\\\"_blank\\\">https://open.dingtalk.com/document/orgapp/obtain-the-webhook-address-of-a-custom-robot</a>\",\r\n  \"access.form.dingtalkbot_secret.label\": \"钉钉群机器人签名密钥（可选）\",\r\n  \"access.form.dingtalkbot_secret.placeholder\": \"请输入钉钉群机器人签名密钥\",\r\n  \"access.form.dingtalkbot_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://open.dingtalk.com/document/orgapp/customize-robot-security-settings\\\" target=\\\"_blank\\\">https://open.dingtalk.com/document/orgapp/customize-robot-security-settings</a>\",\r\n  \"access.form.dingtalkbot_custom_payload.label\": \"钉钉群机器人消息数据（可选）\",\r\n  \"access.form.dingtalkbot_custom_payload.placeholder\": \"请输入自定义的钉钉群机器人消息数据\",\r\n  \"access.form.dingtalkbot_custom_payload.checkbox\": \"使用自定义的消息数据格式\",\r\n  \"access.form.discordbot_token.label\": \"Discord 机器人 API Token\",\r\n  \"access.form.discordbot_token.placeholder\": \"请输入 Discord 机器人 API Token\",\r\n  \"access.form.discordbot_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.discordbotstudio.org/setting-up-dbs/finding-your-bot-token\\\" target=\\\"_blank\\\">https://docs.discordbotstudio.org/setting-up-dbs/finding-your-bot-token</a>\",\r\n  \"access.form.discordbot_channel_id.label\": \"Discord 频道 ID（可选）\",\r\n  \"access.form.discordbot_channel_id.placeholder\": \"请输入默认的 Discord 频道 ID\",\r\n  \"access.form.discordbot_channel_id.help\": \"提示：可在工作流中覆盖此设置。\",\r\n  \"access.form.discordbot_channel_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID\\\" target=\\\"_blank\\\">https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID</a>\",\r\n  \"access.form.dnsmadeeasy_api_key.label\": \"DNS Made Easy API Key\",\r\n  \"access.form.dnsmadeeasy_api_key.placeholder\": \"请输入 DNS Made Easy API Key\",\r\n  \"access.form.dnsmadeeasy_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://api-docs.dnsmadeeasy.com/#5b98221f-37e9-4845-a349-5e959241b4a5\\\" target=\\\"_blank\\\">https://api-docs.dnsmadeeasy.com/#Authentication</a>\",\r\n  \"access.form.dnsmadeeasy_api_secret.label\": \"DNS Made Easy API Secret\",\r\n  \"access.form.dnsmadeeasy_api_secret.placeholder\": \"请输入 DNS Made Easy API Secret\",\r\n  \"access.form.dnsmadeeasy_api_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://api-docs.dnsmadeeasy.com/#5b98221f-37e9-4845-a349-5e959241b4a5\\\" target=\\\"_blank\\\">https://api-docs.dnsmadeeasy.com/#Authentication</a>\",\r\n  \"access.form.dnsexit_api_key.label\": \"DNSExit API Key\",\r\n  \"access.form.dnsexit_api_key.placeholder\": \"请输入 DNSExit API Key\",\r\n  \"access.form.dnsexit_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://dnsexit.com/Direct.sv?cmd=userApiKey\\\" target=\\\"_blank\\\">https://dnsexit.com/Direct.sv?cmd=userApiKey</a>\",\r\n  \"access.form.dnsla_api_id.label\": \"帝恩爱斯 API ID\",\r\n  \"access.form.dnsla_api_id.placeholder\": \"请输入帝恩爱斯 API ID\",\r\n  \"access.form.dnsla_api_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.dns.la/docs/ApiDoc\\\" target=\\\"_blank\\\">https://www.dns.la/docs/ApiDoc</a>\",\r\n  \"access.form.dnsla_api_secret.label\": \"帝恩爱斯 API 密钥\",\r\n  \"access.form.dnsla_api_secret.placeholder\": \"请输入帝恩爱斯 API 密钥\",\r\n  \"access.form.dnsla_api_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.dns.la/docs/ApiDoc\\\" target=\\\"_blank\\\">https://www.dns.la/docs/ApiDoc</a>\",\r\n  \"access.form.dogecloud_access_key.label\": \"多吉云 AccessKey\",\r\n  \"access.form.dogecloud_access_key.placeholder\": \"请输入多吉云 AccessKey\",\r\n  \"access.form.dogecloud_access_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.dogecloud.com/\\\" target=\\\"_blank\\\">https://console.dogecloud.com/</a>\",\r\n  \"access.form.dogecloud_secret_key.label\": \"多吉云 SecretKey\",\r\n  \"access.form.dogecloud_secret_key.placeholder\": \"请输入多吉云 SecretKey\",\r\n  \"access.form.dogecloud_secret_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.dogecloud.com/\\\" target=\\\"_blank\\\">https://console.dogecloud.com/</a>\",\r\n  \"access.form.dokploy_server_url.label\": \"Dokploy 服务地址\",\r\n  \"access.form.dokploy_server_url.placeholder\": \"请输入 Dokploy 服务地址\",\r\n  \"access.form.dokploy_api_key.label\": \"Dokploy API Key\",\r\n  \"access.form.dokploy_api_key.placeholder\": \"请输入 Dokploy API Key\",\r\n  \"access.form.dokploy_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.dokploy.com/docs/api\\\" target=\\\"_blank\\\">https://docs.dokploy.com/docs/api</a>\",\r\n  \"access.form.duckdns_token.label\": \"DuckDNS Token\",\r\n  \"access.form.duckdns_token.placeholder\": \"请输入 DuckDNS Token\",\r\n  \"access.form.duckdns_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.duckdns.org/spec.jsp\\\" target=\\\"_blank\\\">https://www.duckdns.org/spec.jsp</a>\",\r\n  \"access.form.dynu_api_key.label\": \"Dynu API Key\",\r\n  \"access.form.dynu_api_key.placeholder\": \"请输入 Dynu API Key\",\r\n  \"access.form.dynu_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.dynu.com/Support/API#Authentication\\\" target=\\\"_blank\\\">https://www.dynu.com/Support/API#Authentication</a>\",\r\n  \"access.form.dynv6_http_token.label\": \"dynv6 HTTP Token\",\r\n  \"access.form.dynv6_http_token.placeholder\": \"请输入 dynv6 HTTP Token\",\r\n  \"access.form.dynv6_http_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://dynv6.com/keys\\\" target=\\\"_blank\\\">https://dynv6.com/keys</a>\",\r\n  \"access.form.email_smtp_host.label\": \"SMTP 服务器地址\",\r\n  \"access.form.email_smtp_host.placeholder\": \"请输入 SMTP 服务器地址\",\r\n  \"access.form.email_smtp_port.label\": \"SMTP 服务器端口\",\r\n  \"access.form.email_smtp_port.placeholder\": \"请输入 SMTP 服务器端口\",\r\n  \"access.form.email_smtp_tls.label\": \"连接安全性\",\r\n  \"access.form.email_smtp_tls.placeholder\": \"请选择连接安全性\",\r\n  \"access.form.email_smtp_tls.option.true.label\": \"强制 SSL/TLS 连接\",\r\n  \"access.form.email_smtp_tls.option.false.label\": \"优先 STARTTLS，失败则回退为明文连接\",\r\n  \"access.form.email_username.label\": \"用户名\",\r\n  \"access.form.email_username.placeholder\": \"请输入用户名\",\r\n  \"access.form.email_password.label\": \"密码\",\r\n  \"access.form.email_password.placeholder\": \"请输入密码\",\r\n  \"access.form.email_sender_address.label\": \"发件人邮箱\",\r\n  \"access.form.email_sender_address.placeholder\": \"请输入发件人邮箱\",\r\n  \"access.form.email_sender_name.label\": \"发件人名称（可选）\",\r\n  \"access.form.email_sender_name.placeholder\": \"请输入发件人名称\",\r\n  \"access.form.email_receiver_address.label\": \"收件人邮箱（可选）\",\r\n  \"access.form.email_receiver_address.placeholder\": \"请输入默认的收件人邮箱\",\r\n  \"access.form.email_receiver_address.help\": \"提示：可在工作流中覆盖此设置。\",\r\n  \"access.form.flexcdn_server_url.label\": \"FlexCDN 服务地址\",\r\n  \"access.form.flexcdn_server_url.placeholder\": \"请输入 FlexCDN 服务地址\",\r\n  \"access.form.flexcdn_api_role.label\": \"FlexCDN 用户角色\",\r\n  \"access.form.flexcdn_api_role.placeholder\": \"请选择 FlexCDN 用户角色\",\r\n  \"access.form.flexcdn_api_role.option.user.label\": \"平台用户\",\r\n  \"access.form.flexcdn_api_role.option.admin.label\": \"系统管理员\",\r\n  \"access.form.flexcdn_access_key_id.label\": \"FlexCDN AccessKeyID\",\r\n  \"access.form.flexcdn_access_key_id.placeholder\": \"请输入 FlexCDN AccessKeyID\",\r\n  \"access.form.flexcdn_access_key_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://flexcdn.cn/docs/api/auth\\\" target=\\\"_blank\\\">https://flexcdn.cn/docs/api/auth</a>\",\r\n  \"access.form.flexcdn_access_key.label\": \"FlexCDN AccessKey\",\r\n  \"access.form.flexcdn_access_key.placeholder\": \"请输入 FlexCDN AccessKey\",\r\n  \"access.form.flexcdn_access_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://flexcdn.cn/docs/api/auth\\\" target=\\\"_blank\\\">https://flexcdn.cn/docs/api/auth</a>\",\r\n  \"access.form.flyio_api_token.label\": \"Fly.io API Token\",\r\n  \"access.form.flyio_api_token.placeholder\": \"请输入 Fly.io API Token\",\r\n  \"access.form.flyio_api_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://fly.io/docs/security/tokens/\\\" target=\\\"_blank\\\">https://fly.io/docs/security/tokens/</a>\",\r\n  \"access.form.gandinet_personal_access_token.label\": \"Gandi.net Personal Access Token\",\r\n  \"access.form.gandinet_personal_access_token.placeholder\": \"请输入 Gandi.net Personal Access Token\",\r\n  \"access.form.gandinet_personal_access_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://api.gandi.net/docs/authentication/\\\" target=\\\"_blank\\\">https://api.gandi.net/docs/authentication/</a>\",\r\n  \"access.form.gcore_api_token.label\": \"G-Core API Token\",\r\n  \"access.form.gcore_api_token.placeholder\": \"请输入 G-Core API Token\",\r\n  \"access.form.gcore_api_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://api.gcore.com/docs/iam#section/Authentication\\\" target=\\\"_blank\\\">https://api.gcore.com/docs/iam#section/Authentication</a>\",\r\n  \"access.form.gname_app_id.label\": \"GNAME AppID\",\r\n  \"access.form.gname_app_id.placeholder\": \"请输入 GNAME AppID\",\r\n  \"access.form.gname_app_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.gname.com/user#/dealer_api\\\" target=\\\"_blank\\\">https://www.gname.com/user#/dealer_api</a>\",\r\n  \"access.form.gname_app_key.label\": \"GNAME AppKey\",\r\n  \"access.form.gname_app_key.placeholder\": \"请输入 GNAME AppKey\",\r\n  \"access.form.gname_app_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.gname.com/user#/dealer_api\\\" target=\\\"_blank\\\">https://www.gname.com/user#/dealer_api</a>\",\r\n  \"access.form.godaddy_api_key.label\": \"GoDaddy API Key\",\r\n  \"access.form.godaddy_api_key.placeholder\": \"请输入 GoDaddy API Key\",\r\n  \"access.form.godaddy_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://developer.godaddy.com/\\\" target=\\\"_blank\\\">https://developer.godaddy.com/</a>\",\r\n  \"access.form.godaddy_api_secret.label\": \"GoDaddy API Secret\",\r\n  \"access.form.godaddy_api_secret.placeholder\": \"请输入 GoDaddy API Secret\",\r\n  \"access.form.godaddy_api_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://developer.godaddy.com/\\\" target=\\\"_blank\\\">https://developer.godaddy.com/</a>\",\r\n  \"access.form.goedge_server_url.label\": \"GoEdge 服务地址\",\r\n  \"access.form.goedge_server_url.placeholder\": \"请输入 GoEdge 服务地址\",\r\n  \"access.form.goedge_api_role.label\": \"GoEdge 用户角色\",\r\n  \"access.form.goedge_api_role.placeholder\": \"请选择 GoEdge 用户角色\",\r\n  \"access.form.goedge_api_role.option.user.label\": \"平台用户\",\r\n  \"access.form.goedge_api_role.option.admin.label\": \"系统管理员\",\r\n  \"access.form.goedge_access_key_id.label\": \"GoEdge AccessKeyID\",\r\n  \"access.form.goedge_access_key_id.placeholder\": \"请输入 GoEdge AccessKeyID\",\r\n  \"access.form.goedge_access_key_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://goedge.cloud/docs/API/Auth.md\\\" target=\\\"_blank\\\">https://goedge.cloud/docs/API/Auth.md</a>\",\r\n  \"access.form.goedge_access_key.label\": \"GoEdge AccessKey\",\r\n  \"access.form.goedge_access_key.placeholder\": \"请输入 GoEdge AccessKey\",\r\n  \"access.form.goedge_access_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://goedge.cloud/docs/API/Auth.md\\\" target=\\\"_blank\\\">https://goedge.cloud/docs/API/Auth.md</a>\",\r\n  \"access.form.globalsignatlas_eab.guide\": \"点击下方链接了解如何获取 GlobalSign Atlas EAB：<br><a href=\\\"https://globalsign.cn/acme-automated-certificate-management\\\" target=\\\"_blank\\\">https://globalsign.cn/acme-automated-certificate-management</a>\",\r\n  \"access.form.googletrustservices_eab.guide\": \"点击下方链接了解如何获取 Google Trust Services EAB：<br><a href=\\\"https://cloud.google.com/certificate-manager/docs/public-ca-tutorial\\\" target=\\\"_blank\\\">https://cloud.google.com/certificate-manager/docs/public-ca-tutorial</a>\",\r\n  \"access.form.hetzner_api_token.label\": \"Hetzner API Token\",\r\n  \"access.form.hetzner_api_token.placeholder\": \"请输入 Hetzner API Token\",\r\n  \"access.form.hetzner_api_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.hetzner.com/cloud/api/getting-started/generating-api-token\\\" target=\\\"_blank\\\">https://docs.hetzner.com/cloud/api/getting-started/generating-api-token</a>\",\r\n  \"access.form.hostingde_api_key.label\": \"hosting.de API Key\",\r\n  \"access.form.hostingde_api_key.placeholder\": \"请输入 hosting.de API Key\",\r\n  \"access.form.hostingde_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.hosting.de/api/#requests-and-authentication\\\" target=\\\"_blank\\\">https://www.hosting.de/api/#requests-and-authentication</a>\",\r\n  \"access.form.hostinger_api_token.label\": \"Hostinger API Token\",\r\n  \"access.form.hostinger_api_token.placeholder\": \"请输入 Hostinger API Token\",\r\n  \"access.form.hostinger_api_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://developers.hostinger.com/#description/authentication\\\" target=\\\"_blank\\\">https://developers.hostinger.com/#description/authentication</a>\",\r\n  \"access.form.huaweicloud_access_key_id.label\": \"华为云 AccessKeyID\",\r\n  \"access.form.huaweicloud_access_key_id.placeholder\": \"请输入华为云 AccessKeyID\",\r\n  \"access.form.huaweicloud_access_key_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html\\\" target=\\\"_blank\\\">https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html</a>\",\r\n  \"access.form.huaweicloud_secret_access_key.label\": \"华为云 SecretAccessKey\",\r\n  \"access.form.huaweicloud_secret_access_key.placeholder\": \"请输入华为云 SecretAccessKey\",\r\n  \"access.form.huaweicloud_secret_access_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html\\\" target=\\\"_blank\\\">https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html</a>\",\r\n  \"access.form.huaweicloud_enterprise_project_id.label\": \"华为云企业项目 ID（可选）\",\r\n  \"access.form.huaweicloud_enterprise_project_id.placeholder\": \"请输入华为云企业项目 ID\",\r\n  \"access.form.huaweicloud_enterprise_project_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://support.huaweicloud.com/usermanual-em/zh-cn_topic_0126101490.html\\\" target=\\\"_blank\\\">https://support.huaweicloud.com/usermanual-em/zh-cn_topic_0126101490.html</a>\",\r\n  \"access.form.infomaniak_access_token.label\": \"Infomaniak AccessToken\",\r\n  \"access.form.infomaniak_access_token.placeholder\": \"请输入 Infomaniak AccessToken\",\r\n  \"access.form.infomaniak_access_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://manager.infomaniak.com/v3/infomaniak-api\\\" target=\\\"_blank\\\">https://manager.infomaniak.com/v3/infomaniak-api</a>\",\r\n  \"access.form.ionos_api_key_public_prefix.label\": \"IONOS API Key Public Prefix\",\r\n  \"access.form.ionos_api_key_public_prefix.placeholder\": \"请输入 IONOS API Key Public Prefix\",\r\n  \"access.form.ionos_api_key_public_prefix.tooltip\": \"这是什么？请参阅 <a href=\\\"https://developer.hosting.ionos.com/docs/getstarted\\\" target=\\\"_blank\\\">https://developer.hosting.ionos.com/docs/getstarted</a>\",\r\n  \"access.form.ionos_api_key_secret.label\": \"IONOS API Key Secret\",\r\n  \"access.form.ionos_api_key_secret.placeholder\": \"请输入 IONOS API Key Secret\",\r\n  \"access.form.ionos_api_key_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://developer.hosting.ionos.com/docs/getstarted\\\" target=\\\"_blank\\\">https://developer.hosting.ionos.com/docs/getstarted</a>\",\r\n  \"access.form.jdcloud_access_key_id.label\": \"京东云 AccessKeyID\",\r\n  \"access.form.jdcloud_access_key_id.placeholder\": \"请输入京东云 AccessKeyID\",\r\n  \"access.form.jdcloud_access_key_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.jdcloud.com/cn/account-management/accesskey-management\\\" target=\\\"_blank\\\">https://docs.jdcloud.com/cn/account-management/accesskey-management</a>\",\r\n  \"access.form.jdcloud_access_key_secret.label\": \"京东云 AccessKeySecret\",\r\n  \"access.form.jdcloud_access_key_secret.placeholder\": \"请输入京东云 AccessKeySecret\",\r\n  \"access.form.jdcloud_access_key_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.jdcloud.com/cn/account-management/accesskey-management\\\" target=\\\"_blank\\\">https://docs.jdcloud.com/cn/account-management/accesskey-management</a>\",\r\n  \"access.form.k8s_kubeconfig.label\": \"KubeConfig（可选）\",\r\n  \"access.form.k8s_kubeconfig.placeholder\": \"请输入 KubeConfig 文件内容\",\r\n  \"access.form.k8s_kubeconfig.help\": \"提示：不填写时，将使用 Pod 的 ServiceAccount 作为凭证。\",\r\n  \"access.form.k8s_kubeconfig.tooltip\": \"这是什么？请参阅 <a href=\\\"https://kubernetes.io/zh-cn/docs/concepts/configuration/organize-cluster-access-kubeconfig/\\\" target=\\\"_blank\\\">https://kubernetes.io/zh-cn/docs/concepts/configuration/organize-cluster-access-kubeconfig/</a>\",\r\n  \"access.form.kong_server_url.label\": \"Kong Admin API 服务地址\",\r\n  \"access.form.kong_server_url.placeholder\": \"请输入 Kong Admin API 服务地址\",\r\n  \"access.form.kong_api_token.label\": \"Kong Admin API Token（可选）\",\r\n  \"access.form.kong_api_token.placeholder\": \"请输入 Kong Admin API Token\",\r\n  \"access.form.kong_api_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://developer.konghq.com/admin-api/\\\" target=\\\"_blank\\\">https://developer.konghq.com/admin-api/</a>\",\r\n  \"access.form.ksyun_access_key_id.label\": \"金山云 AccessKeyID\",\r\n  \"access.form.ksyun_access_key_id.placeholder\": \"请输入金山云 AccessKeyID\",\r\n  \"access.form.ksyun_access_key_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.ksyun.com/documents/39976\\\" target=\\\"_blank\\\">https://docs.ksyun.com/documents/39976</a>\",\r\n  \"access.form.ksyun_secret_access_key.label\": \"金山云 SecretAccessKey\",\r\n  \"access.form.ksyun_secret_access_key.placeholder\": \"请输入金山云 SecretAccessKey\",\r\n  \"access.form.ksyun_secret_access_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.ksyun.com/documents/39976\\\" target=\\\"_blank\\\">https://docs.ksyun.com/documents/39976</a>\",\r\n  \"access.form.larkbot_webhook_url.label\": \"飞书群机器人 Webhook 地址\",\r\n  \"access.form.larkbot_webhook_url.placeholder\": \"请输入飞书群机器人 Webhook 地址\",\r\n  \"access.form.larkbot_webhook_url.tooltip\": \"这是什么？请参阅 <a href=\\\"https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot\\\" target=\\\"_blank\\\">https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot</a>\",\r\n  \"access.form.larkbot_secret.label\": \"飞书群机器人签名密钥（可选）\",\r\n  \"access.form.larkbot_secret.placeholder\": \"请输入飞书群机器人签名密钥\",\r\n  \"access.form.larkbot_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot\\\" target=\\\"_blank\\\">https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot</a>\",\r\n  \"access.form.larkbot_custom_payload.label\": \"飞书群机器人消息数据（可选）\",\r\n  \"access.form.larkbot_custom_payload.placeholder\": \"请输入自定义的飞书群机器人消息数据\",\r\n  \"access.form.larkbot_custom_payload.checkbox\": \"使用自定义的消息数据格式\",\r\n  \"access.form.lecdn_server_url.label\": \"LeCDN 服务地址\",\r\n  \"access.form.lecdn_server_url.placeholder\": \"请输入 LeCDN 服务地址\",\r\n  \"access.form.lecdn_api_version.label\": \"LeCDN 版本\",\r\n  \"access.form.lecdn_api_version.placeholder\": \"请选择 LeCDN 版本\",\r\n  \"access.form.lecdn_api_role.label\": \"LeCDN 用户角色\",\r\n  \"access.form.lecdn_api_role.placeholder\": \"请选择 LeCDN 用户角色\",\r\n  \"access.form.lecdn_api_role.option.client.label\": \"客户用户\",\r\n  \"access.form.lecdn_api_role.option.master.label\": \"主控管理员\",\r\n  \"access.form.lecdn_username.label\": \"LeCDN 用户名\",\r\n  \"access.form.lecdn_username.placeholder\": \"请输入 LeCDN 用户名\",\r\n  \"access.form.lecdn_password.label\": \"LeCDN 用户密码\",\r\n  \"access.form.lecdn_password.placeholder\": \"请输入 LeCDN 用户密码\",\r\n  \"access.form.linode_access_token.label\": \"Linode AccessToken\",\r\n  \"access.form.linode_access_token.placeholder\": \"请输入 Linode AccessToken\",\r\n  \"access.form.linode_access_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://techdocs.akamai.com/linode-api/reference/get-started\\\" target=\\\"_blank\\\">https://techdocs.akamai.com/linode-api/reference/get-started</a>\",\r\n  \"access.form.litessl_eab.guide\": \"点击下方链接了解如何获取 LiteSSL EAB：<br><a href=\\\"https://freessl.cn/automation/eab-manager\\\" target=\\\"_blank\\\">https://freessl.cn/automation/eab-manager</a>\",\r\n  \"access.form.mattermost_server_url.label\": \"Mattermost 服务地址\",\r\n  \"access.form.mattermost_server_url.placeholder\": \"请输入 Mattermost 服务地址\",\r\n  \"access.form.mattermost_username.label\": \"Mattermost 用户名\",\r\n  \"access.form.mattermost_username.placeholder\": \"请输入 Mattermost 用户名\",\r\n  \"access.form.mattermost_password.label\": \"Mattermost 密码\",\r\n  \"access.form.mattermost_password.placeholder\": \"请输入 Mattermost 密码\",\r\n  \"access.form.mattermost_channel_id.label\": \"Mattermost 频道 ID（可选）\",\r\n  \"access.form.mattermost_channel_id.placeholder\": \"请输入默认的 Mattermost 频道 ID\",\r\n  \"access.form.mattermost_channel_id.help\": \"提示：可在工作流中覆盖此设置。\",\r\n  \"access.form.mattermost_channel_id.tooltip\": \"如何获取此参数？从左侧边栏中选择目标频道，点击顶部的频道名称，选择“频道详情”，即可在弹出页面中直接看到频道 ID。\",\r\n  \"access.form.mohua_username.label\": \"嘿华云用户名\",\r\n  \"access.form.mohua_username.placeholder\": \"请输入嘿华云用户名\",\r\n  \"access.form.mohua_api_password.label\": \"嘿华云 API 密码\",\r\n  \"access.form.mohua_api_password.placeholder\": \"请输入嘿华云 API 密码\",\r\n  \"access.form.mohua_api_password.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.mhjz1.cn/apimanage\\\" target=\\\"_blank\\\">https://cloud.mhjz1.cn/apimanage</a>\",\r\n  \"access.form.namecheap_username.label\": \"Namecheap 用户名\",\r\n  \"access.form.namecheap_username.placeholder\": \"请输入 Namecheap 用户名\",\r\n  \"access.form.namecheap_username.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.namecheap.com/support/api/intro/\\\" target=\\\"_blank\\\">https://www.namecheap.com/support/api/intro/</a>\",\r\n  \"access.form.namecheap_api_key.label\": \"Namecheap API Key\",\r\n  \"access.form.namecheap_api_key.placeholder\": \"请输入 Namecheap API Key\",\r\n  \"access.form.namecheap_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.namecheap.com/support/api/intro/\\\" target=\\\"_blank\\\">https://www.namecheap.com/support/api/intro/</a>\",\r\n  \"access.form.namedotcom_username.label\": \"Name.com 用户名\",\r\n  \"access.form.namedotcom_username.placeholder\": \"请输入 Name.com 用户名\",\r\n  \"access.form.namedotcom_username.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.name.com/account/settings/api\\\" target=\\\"_blank\\\">https://www.name.com/account/settings/api</a>\",\r\n  \"access.form.namedotcom_api_token.label\": \"Name.com API Token\",\r\n  \"access.form.namedotcom_api_token.placeholder\": \"请输入 Name.com API Token\",\r\n  \"access.form.namedotcom_api_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.name.com/support/articles/31142639244819-how-to-manage-your-api-tokens\\\" target=\\\"_blank\\\">https://www.name.com/support/articles/31142639244819-how-to-manage-your-api-tokens</a>\",\r\n  \"access.form.namesilo_api_key.label\": \"NameSilo API Key\",\r\n  \"access.form.namesilo_api_key.placeholder\": \"请输入 NameSilo API Key\",\r\n  \"access.form.namesilo_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.namesilo.com/support/v2/articles/account-options/api-manager\\\" target=\\\"_blank\\\">https://www.namesilo.com/support/v2/articles/account-options/api-manager</a>\",\r\n  \"access.form.netlify_api_token.label\": \"Netlify API Token\",\r\n  \"access.form.netlify_api_token.placeholder\": \"请输入 Netlify API Token\",\r\n  \"access.form.netlify_api_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.netlify.com/api/get-started/#authentication\\\" target=\\\"_blank\\\">https://docs.netlify.com/api/get-started/#authentication</a>\",\r\n  \"access.form.netcup_customer_number.label\": \"netcup 客户编号\",\r\n  \"access.form.netcup_customer_number.placeholder\": \"请输入 netcup 客户编号\",\r\n  \"access.form.netcup_customer_number.tooltip\": \"这是什么？请参阅 <a href=\\\"https://helpcenter.netcup.com/en/wiki/general/ccp-login/\\\" target=\\\"_blank\\\">https://helpcenter.netcup.com/en/wiki/general/ccp-login/</a>\",\r\n  \"access.form.netcup_api_key.label\": \"netcup API Key\",\r\n  \"access.form.netcup_api_key.placeholder\": \"请输入 netcup API Key\",\r\n  \"access.form.netcup_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://helpcenter.netcup.com/en/wiki/general/our-api/\\\" target=\\\"_blank\\\">https://helpcenter.netcup.com/en/wiki/general/our-api/</a>\",\r\n  \"access.form.netcup_api_password.label\": \"netcup API Key 密码\",\r\n  \"access.form.netcup_api_password.placeholder\": \"请输入 netcup API Key 密码\",\r\n  \"access.form.netcup_api_password.tooltip\": \"这是什么？请参阅 <a href=\\\"https://helpcenter.netcup.com/en/wiki/general/our-api/\\\" target=\\\"_blank\\\">https://helpcenter.netcup.com/en/wiki/general/our-api/</a>\",\r\n  \"access.form.nginxproxymanager_server_url.label\": \"NPM 服务地址\",\r\n  \"access.form.nginxproxymanager_server_url.placeholder\": \"请输入 NPM 服务地址\",\r\n  \"access.form.nginxproxymanager_auth_method.label\": \"NPM API 认证方式\",\r\n  \"access.form.nginxproxymanager_auth_method.placeholder\": \"请选择 NPM API 认证方式\",\r\n  \"access.form.nginxproxymanager_auth_method.option.password.label\": \"密码\",\r\n  \"access.form.nginxproxymanager_auth_method.option.token.label\": \"API Token\",\r\n  \"access.form.nginxproxymanager_username.label\": \"NPM 管理员用户名\",\r\n  \"access.form.nginxproxymanager_username.placeholder\": \"请输入 NPM 管理员用户名\",\r\n  \"access.form.nginxproxymanager_password.label\": \"NPM 管理员密码\",\r\n  \"access.form.nginxproxymanager_password.placeholder\": \"请输入 NPM 管理员密码\",\r\n  \"access.form.nginxproxymanager_api_token.label\": \"NPM API Token\",\r\n  \"access.form.nginxproxymanager_api_token.placeholder\": \"请输入 NPM API Token\",\r\n  \"access.form.ns1_api_key.label\": \"NS1 API Key\",\r\n  \"access.form.ns1_api_key.placeholder\": \"请输入 NS1 API Key\",\r\n  \"access.form.ns1_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.ibm.com/docs/zh/ns1-connect?topic=introduction-using-api\\\" target=\\\"_blank\\\">https://www.ibm.com/docs/zh/ns1-connect?topic=introduction-using-api</a>\",\r\n  \"access.form.ovhcloud_endpoint.label\": \"OVHcloud API 端点\",\r\n  \"access.form.ovhcloud_endpoint.placeholder\": \"请输入 OVHcloud API 端点\",\r\n  \"access.form.ovhcloud_auth_method.label\": \"OVHcloud API 认证方式\",\r\n  \"access.form.ovhcloud_auth_method.placeholder\": \"请选择 OVHcloud API 认证方式\",\r\n  \"access.form.ovhcloud_auth_method.option.application.label\": \"Application Key & Secret\",\r\n  \"access.form.ovhcloud_auth_method.option.oauth2.label\": \"OAuth2 Client Credentials\",\r\n  \"access.form.ovhcloud_application_key.label\": \"OVHcloud Application Key\",\r\n  \"access.form.ovhcloud_application_key.placeholder\": \"请输入 OVHcloud Application Key\",\r\n  \"access.form.ovhcloud_application_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/\\\" target=\\\"_blank\\\">https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/</a>\",\r\n  \"access.form.ovhcloud_application_secret.label\": \"OVHcloud Application Secret\",\r\n  \"access.form.ovhcloud_application_secret.placeholder\": \"请输入 OVHcloud Application Secret\",\r\n  \"access.form.ovhcloud_application_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/\\\" target=\\\"_blank\\\">https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/</a>\",\r\n  \"access.form.ovhcloud_consumer_key.label\": \"OVHcloud Consumer Key\",\r\n  \"access.form.ovhcloud_consumer_key.placeholder\": \"请输入 OVHcloud Consumer Key\",\r\n  \"access.form.ovhcloud_consumer_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/\\\" target=\\\"_blank\\\">https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/</a>\",\r\n  \"access.form.ovhcloud_client_id.label\": \"OVHcloud Client ID\",\r\n  \"access.form.ovhcloud_client_id.placeholder\": \"请输入 OVHcloud Client ID\",\r\n  \"access.form.ovhcloud_client_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343\\\" target=\\\"_blank\\\">https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343</a>\",\r\n  \"access.form.ovhcloud_client_secret.label\": \"OVHcloud Client Cecret\",\r\n  \"access.form.ovhcloud_client_secret.placeholder\": \"请输入 OVHcloud Client Secret\",\r\n  \"access.form.ovhcloud_client_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343\\\" target=\\\"_blank\\\">https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343</a>\",\r\n  \"access.form.porkbun_api_key.label\": \"Porkbun API Key\",\r\n  \"access.form.porkbun_api_key.placeholder\": \"请输入 Porkbun API Key\",\r\n  \"access.form.porkbun_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://porkbun.com/api/json/v3/documentation#Authentication\\\" target=\\\"_blank\\\">https://porkbun.com/api/json/v3/documentation</a>\",\r\n  \"access.form.porkbun_secret_api_key.label\": \"Porkbun Secret API Key\",\r\n  \"access.form.porkbun_secret_api_key.placeholder\": \"请输入 Porkbun Secret API Key\",\r\n  \"access.form.porkbun_secret_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://porkbun.com/api/json/v3/documentation#Authentication\\\" target=\\\"_blank\\\">https://porkbun.com/api/json/v3/documentation</a>\",\r\n  \"access.form.powerdns_server_url.label\": \"PowerDNS 服务地址\",\r\n  \"access.form.powerdns_server_url.placeholder\": \"请输入 PowerDNS 服务地址\",\r\n  \"access.form.powerdns_api_key.label\": \"PowerDNS API Key\",\r\n  \"access.form.powerdns_api_key.placeholder\": \"请输入 PowerDNS API Key\",\r\n  \"access.form.powerdns_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://doc.powerdns.com/authoritative/http-api/index.html#enabling-the-api\\\" target=\\\"_blank\\\">https://doc.powerdns.com/authoritative/http-api/index.html#enabling-the-api</a>\",\r\n  \"access.form.proxmoxve_server_url.label\": \"Proxmox VE 服务地址\",\r\n  \"access.form.proxmoxve_server_url.placeholder\": \"请输入 Proxmox VE 服务地址\",\r\n  \"access.form.proxmoxve_api_token.label\": \"Proxmox VE API Token\",\r\n  \"access.form.proxmoxve_api_token.placeholder\": \"请输入 Proxmox VE API Token\",\r\n  \"access.form.proxmoxve_api_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens\\\" target=\\\"_blank\\\">https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens</a>\",\r\n  \"access.form.proxmoxve_api_token_secret.label\": \"Proxmox VE API Token Secret（可选）\",\r\n  \"access.form.proxmoxve_api_token_secret.placeholder\": \"请输入 Proxmox VE API Token Secret\",\r\n  \"access.form.proxmoxve_api_token_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens\\\" target=\\\"_blank\\\">https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens</a>\",\r\n  \"access.form.qingcloud_access_key_id.label\": \"青云 AccessKeyID\",\r\n  \"access.form.qingcloud_access_key_id.placeholder\": \"请输入青云 AccessKeyID\",\r\n  \"access.form.qingcloud_access_key_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.qingcloud.com/access_keys/\\\" target=\\\"_blank\\\">https://console.qingcloud.com/access_keys/</a>\",\r\n  \"access.form.qingcloud_secret_access_key.label\": \"青云 SecretAccessKey\",\r\n  \"access.form.qingcloud_secret_access_key.placeholder\": \"请输入青云 SecretAccessKey\",\r\n  \"access.form.qingcloud_secret_access_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.qingcloud.com/access_keys/\\\" target=\\\"_blank\\\">https://console.qingcloud.com/access_keys/</a>\",\r\n  \"access.form.qiniu_access_key.label\": \"七牛云 AccessKey\",\r\n  \"access.form.qiniu_access_key.placeholder\": \"请输入七牛云 AccessKey\",\r\n  \"access.form.qiniu_access_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://portal.qiniu.com/\\\" target=\\\"_blank\\\">https://portal.qiniu.com/</a>\",\r\n  \"access.form.qiniu_secret_key.label\": \"七牛云 SecretKey\",\r\n  \"access.form.qiniu_secret_key.placeholder\": \"请输入七牛云 SecretKey\",\r\n  \"access.form.qiniu_secret_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://portal.qiniu.com/\\\" target=\\\"_blank\\\">https://portal.qiniu.com/</a>\",\r\n  \"access.form.rainyun_api_key.label\": \"雨云 API 密钥\",\r\n  \"access.form.rainyun_api_key.placeholder\": \"请输入雨云 API 密钥\",\r\n  \"access.form.rainyun_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://app.rainyun.com/account/settings/api-key\\\" target=\\\"_blank\\\">https://app.rainyun.com/account/settings/api-key</a>\",\r\n  \"access.form.ratpanel_server_url.label\": \"耗子面板服务地址\",\r\n  \"access.form.ratpanel_server_url.placeholder\": \"请输入耗子面板服务地址\",\r\n  \"access.form.ratpanel_server_url.help\": \"提示：请勿包含安全入口后缀。\",\r\n  \"access.form.ratpanel_access_token_id.label\": \"耗子面板 AccessToken ID\",\r\n  \"access.form.ratpanel_access_token_id.placeholder\": \"请输入耗子面板 AccessToken ID\",\r\n  \"access.form.ratpanel_access_token_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://ratpanel.github.io/advanced/api.html\\\" target=\\\"_blank\\\">https://ratpanel.github.io/advanced/api.html</a>\",\r\n  \"access.form.ratpanel_access_token.label\": \"耗子面板 AccessToken\",\r\n  \"access.form.ratpanel_access_token.placeholder\": \"请输入耗子面板 AccessToken\",\r\n  \"access.form.ratpanel_access_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://ratpanel.github.io/advanced/api.html\\\" target=\\\"_blank\\\">https://ratpanel.github.io/advanced/api.html</a>\",\r\n  \"access.form.rfc2136_host.label\": \"DNS 服务器地址\",\r\n  \"access.form.rfc2136_host.placeholder\": \"请输入 DNS 服务器地址\",\r\n  \"access.form.rfc2136_port.label\": \"DNS 服务器端口\",\r\n  \"access.form.rfc2136_port.placeholder\": \"请输入 DNS 服务器端口\",\r\n  \"access.form.rfc2136_tsig_algorithm.label\": \"TSIG 算法\",\r\n  \"access.form.rfc2136_tsig_algorithm.placeholder\": \"请选择 TSIG 算法\",\r\n  \"access.form.rfc2136_tsig_key.label\": \"TSIG 认证密钥 Key（可选）\",\r\n  \"access.form.rfc2136_tsig_key.placeholder\": \"请输入 TSIG 认证密钥 Key\",\r\n  \"access.form.rfc2136_tsig_secret.label\": \"TSIG 认证密钥 Secret（可选）\",\r\n  \"access.form.rfc2136_tsig_secret.placeholder\": \"请输入 TSIG 认证密钥 Secret\",\r\n  \"access.form.s3_endpoint.label\": \"终端节点\",\r\n  \"access.form.s3_endpoint.placeholder\": \"请输入终端节点\",\r\n  \"access.form.s3_endpoint.help\": \"注意：如果不指定协议，则默认使用 <em>https://</em>。\",\r\n  \"access.form.s3_access_key.label\": \"AccessKey\",\r\n  \"access.form.s3_access_key.placeholder\": \"请输入 AccessKey\",\r\n  \"access.form.s3_secret_key.label\": \"SecretKey\",\r\n  \"access.form.s3_secret_key.placeholder\": \"请输入 SecretKey\",\r\n  \"access.form.s3_signature_version.label\": \"签名版本\",\r\n  \"access.form.s3_signature_version.placeholder\": \"请选择签名版本\",\r\n  \"access.form.s3_use_path_style.label\": \"使用路径风格地址\",\r\n  \"access.form.s3_use_path_style.tooltip\": \"<ol style=\\\"list-style: disc;\\\"><li>虚拟托管风格（默认）：<br><em>https://&lt;BUCKET&gt;.&lt;ENDPOINT&gt;/&lt;KEY&gt;</em> </li><li>路径风格：<br><em>https://&lt;ENDPOINT&gt;/&lt;BUCKET&gt;/&lt;KEY&gt;</em> </li></ol>\",\r\n  \"access.form.safeline_server_url.label\": \"雷池服务地址\",\r\n  \"access.form.safeline_server_url.placeholder\": \"请输入雷池服务地址\",\r\n  \"access.form.safeline_api_token.label\": \"雷池 API Token\",\r\n  \"access.form.safeline_api_token.placeholder\": \"请输入雷池 API Token\",\r\n  \"access.form.safeline_api_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.waf-ce.chaitin.cn/zh/%E6%9B%B4%E5%A4%9A%E6%8A%80%E6%9C%AF%E6%96%87%E6%A1%A3/OPENAPI\\\" target=\\\"_blank\\\">https://docs.waf-ce.chaitin.cn/zh/更多技术文档/OPENAPI</a>\",\r\n  \"access.form.sectigo_validation_type.label\": \"域名验证类型\",\r\n  \"access.form.sectigo_validation_type.placeholder\": \"请选择域名验证类型\",\r\n  \"access.form.sectigo_validation_type.option.dv.label\": \"DV（域名型）\",\r\n  \"access.form.sectigo_validation_type.option.ov.label\": \"OV（企业型）\",\r\n  \"access.form.sectigo_validation_type.option.ev.label\": \"EV（增强型）\",\r\n  \"access.form.sectigo_eab.guide\": \"点击下方链接了解如何获取 Sectigo EAB：<br><a href=\\\"https://www.sectigo.com/enterprise-solutions/certificate-manager/integrations-acme\\\" target=\\\"_blank\\\">https://www.sectigo.com/enterprise-solutions/certificate-manager/integrations-acme</a>\",\r\n  \"access.form.slackbot_token.label\": \"Slack 机器人 Token\",\r\n  \"access.form.slackbot_token.placeholder\": \"请输入 Slack 机器人 Token\",\r\n  \"access.form.slackbot_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.slack.dev/authentication/tokens#bot\\\" target=\\\"_blank\\\">https://docs.slack.dev/authentication/tokens#bot</a>\",\r\n  \"access.form.slackbot_channel_id.label\": \"Slack 频道 ID（可选）\",\r\n  \"access.form.slackbot_channel_id.placeholder\": \"请输入默认的 Slack 频道 ID\",\r\n  \"access.form.slackbot_channel_id.help\": \"提示：可在工作流中覆盖此设置。\",\r\n  \"access.form.slackbot_channel_id.tooltip\": \"如何获取此参数？请参阅 <a href=\\\"https://www.youtube.com/watch?v=Uz5Yi5C2pwQ\\\" target=\\\"_blank\\\">https://www.youtube.com/watch?v=Uz5Yi5C2pwQ</a>\",\r\n  \"access.form.spaceship_api_key.label\": \"Spaceship API Key\",\r\n  \"access.form.spaceship_api_key.placeholder\": \"请输入 Spaceship API Key\",\r\n  \"access.form.spaceship_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.spaceship.com/application/api-manager/\\\" target=\\\"_blank\\\">https://www.spaceship.com/application/api-manager/</a>\",\r\n  \"access.form.spaceship_api_secret.label\": \"Spaceship API Secret\",\r\n  \"access.form.spaceship_api_secret.placeholder\": \"请输入 Spaceship API Secret\",\r\n  \"access.form.spaceship_api_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.spaceship.com/application/api-manager/\\\" target=\\\"_blank\\\">https://www.spaceship.com/application/api-manager/</a>\",\r\n  \"access.form.ssh_host.label\": \"服务器地址\",\r\n  \"access.form.ssh_host.placeholder\": \"请输入服务器地址\",\r\n  \"access.form.ssh_port.label\": \"服务器端口\",\r\n  \"access.form.ssh_port.placeholder\": \"请输入服务器端口\",\r\n  \"access.form.ssh_auth_method.label\": \"认证方式\",\r\n  \"access.form.ssh_auth_method.placeholder\": \"请选择认证方式\",\r\n  \"access.form.ssh_auth_method.option.none.label\": \"无\",\r\n  \"access.form.ssh_auth_method.option.password.label\": \"密码\",\r\n  \"access.form.ssh_auth_method.option.key.label\": \"SSH 密钥\",\r\n  \"access.form.ssh_username.label\": \"用户名\",\r\n  \"access.form.ssh_username.placeholder\": \"请输入用户名\",\r\n  \"access.form.ssh_password.label\": \"密码\",\r\n  \"access.form.ssh_password.placeholder\": \"请输入密码\",\r\n  \"access.form.ssh_key.label\": \"SSH 密钥\",\r\n  \"access.form.ssh_key.placeholder\": \"请输入 SSH 密钥文件内容\",\r\n  \"access.form.ssh_key_passphrase.label\": \"SSH 密钥口令（可选）\",\r\n  \"access.form.ssh_key_passphrase.placeholder\": \"请输入 SSH 密钥口令\",\r\n  \"access.form.ssh_jump_servers.label\": \"跳板机（可选）\",\r\n  \"access.form.ssh_jump_servers.errmsg.invalid\": \"请配置有效的跳板机信息\",\r\n  \"access.form.ssh_jump_servers.item.label\": \"跳板机\",\r\n  \"access.form.ssh_jump_servers.add.button\": \"添加跳板机\",\r\n  \"access.form.sslcom_eab.guide\": \"点击下方链接了解如何获取 SSL.com EAB：<br><a href=\\\"https://www.ssl.com/how-to/generate-acme-credentials-for-reseller-customers/#ftoc-heading-6\\\" target=\\\"_blank\\\">https://www.ssl.com/how-to/generate-acme-credentials-for-reseller-customers/</a>\",\r\n  \"access.form.synologydsm_server_url.label\": \"群晖 DSM 服务地址\",\r\n  \"access.form.synologydsm_server_url.placeholder\": \"请输入群晖 DSM 服务地址\",\r\n  \"access.form.synologydsm_username.label\": \"群晖 DSM 用户名\",\r\n  \"access.form.synologydsm_username.placeholder\": \"请输入群晖 DSM 用户名\",\r\n  \"access.form.synologydsm_password.label\": \"群晖 DSM 用户密码\",\r\n  \"access.form.synologydsm_password.placeholder\": \"请输入群晖 DSM 用户密码\",\r\n  \"access.form.synologydsm_totp_secret.label\": \"群晖 DSM 双重验证 TOTP 密钥（可选）\",\r\n  \"access.form.synologydsm_totp_secret.placeholder\": \"请输入群晖 DSM 双重验证 TOTP 密钥\",\r\n  \"access.form.synologydsm_totp_secret.help\": \"提示：仅当群晖开启双重验证登录时需要填写，用于生成动态登录验证码。\",\r\n  \"access.form.synologydsm_totp_secret.tooltip\": \"如何获取此参数？可在设置双重验证时点击「无法扫描？」查看，或将二维码识别为文本后、提取其中的 <em>secret</em> 部分。注意，这不是 6 位动态 OTP 码。\",\r\n  \"access.form.technitiumdns_server_url.label\": \"Technitium DNS 服务地址\",\r\n  \"access.form.technitiumdns_server_url.placeholder\": \"请输入 Technitium DNS 服务地址\",\r\n  \"access.form.technitiumdns_api_token.label\": \"Technitium DNS API Token\",\r\n  \"access.form.technitiumdns_api_token.placeholder\": \"请输入 Technitium DNS API Token\",\r\n  \"access.form.technitiumdns_api_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://github.com/TechnitiumSoftware/DnsServer/blob/master/APIDOCS.md\\\" target=\\\"_blank\\\">https://github.com/TechnitiumSoftware/DnsServer/blob/master/APIDOCS.md</a>\",\r\n  \"access.form.telegrambot_token.label\": \"Telegram 机器人 API Token\",\r\n  \"access.form.telegrambot_token.placeholder\": \"请输入 Telegram 机器人 API Token\",\r\n  \"access.form.telegrambot_token.tooltip\": \"如何获取此参数？请参阅 <a href=\\\"https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a\\\" target=\\\"_blank\\\">https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a</a>\",\r\n  \"access.form.telegrambot_chat_id.label\": \"Telegram 会话 ID（可选）\",\r\n  \"access.form.telegrambot_chat_id.placeholder\": \"请输入默认的 Telegram 会话 ID\",\r\n  \"access.form.telegrambot_chat_id.help\": \"提示：可在工作流中覆盖此设置。\",\r\n  \"access.form.telegrambot_chat_id.tooltip\": \"如何获取此参数？请参阅 <a href=\\\"https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a\\\" target=\\\"_blank\\\">https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a</a>\",\r\n  \"access.form.tencentcloud_secret_id.label\": \"腾讯云 SecretID\",\r\n  \"access.form.tencentcloud_secret_id.placeholder\": \"请输入腾讯云 SecretID\",\r\n  \"access.form.tencentcloud_secret_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/598/40488\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/598/40488</a>\",\r\n  \"access.form.tencentcloud_secret_key.label\": \"腾讯云 SecretKey\",\r\n  \"access.form.tencentcloud_secret_key.placeholder\": \"请输入腾讯云 SecretKey\",\r\n  \"access.form.tencentcloud_secret_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/598/40488\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/598/40488</a>\",\r\n  \"access.form.todaynic_user_id.label\": \"时代互联代理商用户 ID\",\r\n  \"access.form.todaynic_user_id.placeholder\": \"请输入时代互联代理商用户 ID\",\r\n  \"access.form.todaynic_api_key.label\": \"时代互联代理商 API Key\",\r\n  \"access.form.todaynic_api_key.placeholder\": \"请输入时代互联代理商 API Key\",\r\n  \"access.form.todaynic_agent.guide\": \"时代互联 API 仅支持代理商调用。点击下方链接了解更多：<br><a href=\\\"https://docs.apipost.net/docs/detail/49dcef10a876000?target_id=371b384\\\" target=\\\"_blank\\\">https://docs.apipost.net/docs/detail/49dcef10a876000?target_id=371b384</a>\",\r\n  \"access.form.ucloud_private_key.label\": \"优刻得 API 私钥\",\r\n  \"access.form.ucloud_private_key.placeholder\": \"请输入优刻得 API 私钥\",\r\n  \"access.form.ucloud_private_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.ucloud.cn/uaccount/api_manage\\\" target=\\\"_blank\\\">https://console.ucloud.cn/uaccount/api_manage</a>\",\r\n  \"access.form.ucloud_public_key.label\": \"优刻得 API 公钥\",\r\n  \"access.form.ucloud_public_key.placeholder\": \"请输入优刻得 API 公钥\",\r\n  \"access.form.ucloud_public_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.ucloud.cn/uaccount/api_manage\\\" target=\\\"_blank\\\">https://console.ucloud.cn/uaccount/api_manage</a>\",\r\n  \"access.form.ucloud_project_id.label\": \"优刻得项目 ID（可选）\",\r\n  \"access.form.ucloud_project_id.placeholder\": \"请输入优刻得项目 ID\",\r\n  \"access.form.ucloud_project_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.ucloud.cn/uaccount/iam/project_manage\\\" target=\\\"_blank\\\">https://console.ucloud.cn/uaccount/iam/project_manage</a>\",\r\n  \"access.form.unicloud_username.label\": \"uniCloud 控制台账号\",\r\n  \"access.form.unicloud_username.placeholder\": \"请输入 uniCloud 控制台账号\",\r\n  \"access.form.unicloud_password.label\": \"uniCloud 控制台密码\",\r\n  \"access.form.unicloud_password.placeholder\": \"请输入 uniCloud 控制台密码\",\r\n  \"access.form.upyun_username.label\": \"又拍云子账号用户名\",\r\n  \"access.form.upyun_username.placeholder\": \"请输入又拍云子账号用户名\",\r\n  \"access.form.upyun_username.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.upyun.com/account/subaccount/\\\" target=\\\"_blank\\\">https://console.upyun.com/account/subaccount/</a><br>请关闭该账号的二次登录验证。\",\r\n  \"access.form.upyun_password.label\": \"又拍云子账号密码\",\r\n  \"access.form.upyun_password.placeholder\": \"请输入又拍云子账号密码\",\r\n  \"access.form.upyun_password.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.upyun.com/account/subaccount/\\\" target=\\\"_blank\\\">https://console.upyun.com/account/subaccount/</a><br>请关闭该账号的二次登录验证。\",\r\n  \"access.form.vercel_api_access_token.label\": \"Vercel API Access Token\",\r\n  \"access.form.vercel_api_access_token.placeholder\": \"请输入 Vercel API Access Token\",\r\n  \"access.form.vercel_api_access_token.tooltip\": \"这是什么？请参阅 <a href=\\\"https://vercel.com/guides/how-do-i-use-a-vercel-api-access-token\\\" target=\\\"_blank\\\">https://vercel.com/guides/how-do-i-use-a-vercel-api-access-token</a>\",\r\n  \"access.form.vercel_team_id.label\": \"Vercel 团队 ID（可选）\",\r\n  \"access.form.vercel_team_id.placeholder\": \"请输入 Vercel 团队 ID\",\r\n  \"access.form.vercel_team_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://vercel.com/docs/accounts#find-your-team-id\\\" target=\\\"_blank\\\">https://vercel.com/docs/accounts#find-your-team-id</a>\",\r\n  \"access.form.volcengine_access_key_id.label\": \"火山引擎 AccessKeyID\",\r\n  \"access.form.volcengine_access_key_id.placeholder\": \"请输入火山引擎 AccessKeyID\",\r\n  \"access.form.volcengine_access_key_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.volcengine.com/docs/6291/216571\\\" target=\\\"_blank\\\">https://www.volcengine.com/docs/6291/216571</a>\",\r\n  \"access.form.volcengine_secret_access_key.label\": \"火山引擎 SecretAccessKey\",\r\n  \"access.form.volcengine_secret_access_key.placeholder\": \"请输入火山引擎 SecretAccessKey\",\r\n  \"access.form.volcengine_secret_access_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.volcengine.com/docs/6291/216571\\\" target=\\\"_blank\\\">https://www.volcengine.com/docs/6291/216571</a>\",\r\n  \"access.form.vultr_api_key.label\": \"Vultr API Key\",\r\n  \"access.form.vultr_api_key.placeholder\": \"请输入 Vultr API Key\",\r\n  \"access.form.vultr_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.vultr.com/platform/other/users/manage-users/api-access/regenerate-user-api-key\\\" target=\\\"_blank\\\">https://docs.vultr.com/platform/other/users/manage-users/api-access/regenerate-user-api-key</a>\",\r\n  \"access.form.wangsu_access_key_id.label\": \"网宿云 AccessKeyID\",\r\n  \"access.form.wangsu_access_key_id.placeholder\": \"请输入网宿云 AccessKeyID\",\r\n  \"access.form.wangsu_access_key_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.wangsu.com/document/account-manage/15775\\\" target=\\\"_blank\\\">https://www.wangsu.com/document/account-manage/15775</a>\",\r\n  \"access.form.wangsu_access_key_secret.label\": \"网宿云 AccessKeySecret\",\r\n  \"access.form.wangsu_access_key_secret.placeholder\": \"请输入网宿云 AccessKeySecret\",\r\n  \"access.form.wangsu_access_key_secret.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.wangsu.com/document/account-manage/15775\\\" target=\\\"_blank\\\">https://www.wangsu.com/document/account-manage/15775</a>\",\r\n  \"access.form.wangsu_api_key.label\": \"网宿云 API 接口密码\",\r\n  \"access.form.wangsu_api_key.placeholder\": \"请输入网宿云 API 接口密码\",\r\n  \"access.form.wangsu_api_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.wangsu.com/document/account-manage/15776\\\" target=\\\"_blank\\\">https://www.wangsu.com/document/account-manage/15776</a>\",\r\n  \"access.form.webhook_url.label\": \"Webhook 回调地址\",\r\n  \"access.form.webhook_url.placeholder\": \"请输入 Webhook 回调地址\",\r\n  \"access.form.webhook_method.label\": \"Webhook 请求谓词\",\r\n  \"access.form.webhook_method.placeholder\": \"请选择 Webhook 请求谓词\",\r\n  \"access.form.webhook_headers.label\": \"Webhook 请求标头（可选）\",\r\n  \"access.form.webhook_headers.placeholder\": \"请输入 Webhook 请求标头\",\r\n  \"access.form.webhook_headers.errmsg.invalid\": \"请输入有效的请求标头\",\r\n  \"access.form.webhook_headers.tooltip\": \"示例：<br><i>Content-Type: application/json<br>User-Agent: certimate</i>\",\r\n  \"access.form.webhook_data.label\": \"Webhook 回调数据（可选）\",\r\n  \"access.form.webhook_data.placeholder\": \"请输入默认的 Webhook 回调数据\",\r\n  \"access.form.webhook_data.help\": \"提示：可在工作流中覆盖此设置。\",\r\n  \"access.form.webhook_data.guide_for_deployment\": \"回调数据是一个 JSON 格式的数据。<br><br>其中值支持模板变量，将在被发送到指定的 Webhook URL 时被替换为实际值；其他内容将保持原样。支持的变量：<br><ol style=\\\"list-style: disc;\\\"><li><strong>${CERTIMATE_DEPLOYER_COMMONNAME}</strong>：证书的主域名或 IP。</li><li><strong>${CERTIMATE_DEPLOYER_SUBJECTALTNAMES}</strong>：证书的多域名或 IP，以半角分号隔开。</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE}</strong>：证书文件 PEM 格式内容。</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE_SERVER}</strong>：证书文件（仅含服务器证书）PEM 格式内容。</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE_INTERMEDIA}</strong>：证书文件（仅含中间证书）PEM 格式内容。</li><li><strong>${CERTIMATE_DEPLOYER_PRIVATEKEY}</strong>：私钥文件 PEM 格式内容。</li></ol><br>当请求谓词为 GET 时，回调数据将作为查询参数；否则，回调数据将按照请求标头中 Content-Type 所指示的格式进行编码。支持的格式：<br><ol style=\\\"list-style: disc;\\\"><li>application/json（默认）。</li><li>application/x-www-form-urlencoded：不支持嵌套数据。</li><li>multipart/form-data：不支持嵌套数据。</li>\",\r\n  \"access.form.webhook_data.guide_for_notification\": \"回调数据是一个 JSON 格式的数据。<br><br>其中值支持模板变量，将在被发送到指定的 Webhook URL 时被替换为实际值；其他内容将保持原样。支持的变量：<br><ol style=\\\"list-style: disc;\\\"><li><strong>${CERTIMATE_NOTIFIER_SUBJECT}</strong>：通知主题。</li><li><strong>${CERTIMATE_NOTIFIER_MESSAGE}</strong>：通知内容。</ol><br>当请求谓词为 GET 时，回调数据将作为查询参数；否则，回调数据将按照请求标头中 Content-Type 所指示的格式进行编码。支持的格式：<br><ol style=\\\"list-style: disc;\\\"><li>application/json（默认）。</li><li>application/x-www-form-urlencoded：不支持嵌套数据。</li><li>multipart/form-data：不支持嵌套数据。</li>\",\r\n  \"access.form.webhook_preset_data\": \"使用预设回调\",\r\n  \"access.form.webhook_preset_data.bark\": \"Bark\",\r\n  \"access.form.webhook_preset_data.gotify\": \"Gotify\",\r\n  \"access.form.webhook_preset_data.messagenest\": \"Message Nest\",\r\n  \"access.form.webhook_preset_data.ntfy\": \"ntfy\",\r\n  \"access.form.webhook_preset_data.pushme\": \"PushMe\",\r\n  \"access.form.webhook_preset_data.pushover\": \"Pushover\",\r\n  \"access.form.webhook_preset_data.pushplus\": \"PushPlus 推送加\",\r\n  \"access.form.webhook_preset_data.serverchan3\": \"Server 酱 <sup>3</sup>\",\r\n  \"access.form.webhook_preset_data.serverchanturbo\": \"Server酱 <sup>Turbo</sup>\",\r\n  \"access.form.webhook_preset_data.wxpush\": \"WXPush\",\r\n  \"access.form.webhook_preset_data.common\": \"通用内容\",\r\n  \"access.form.wecombot_webhook_url.label\": \"企业微信群机器人 Webhook 地址\",\r\n  \"access.form.wecombot_webhook_url.placeholder\": \"请输入企业微信群机器人 Webhook 地址\",\r\n  \"access.form.wecombot_webhook_url.tooltip\": \"这是什么？请参阅 <a href=\\\"https://open.work.weixin.qq.com/help2/pc/18401#%E5%85%AD%E3%80%81%E7%BE%A4%E6%9C%BA%E5%99%A8%E4%BA%BAWebhook%E5%9C%B0%E5%9D%80\\\" target=\\\"_blank\\\">https://open.work.weixin.qq.com/help2/pc/18401</a>\",\r\n  \"access.form.wecombot_custom_payload.label\": \"企业微信群机器人消息格式（可选）\",\r\n  \"access.form.wecombot_custom_payload.placeholder\": \"请输入自定义的企业微信群机器人消息格式\",\r\n  \"access.form.wecombot_custom_payload.checkbox\": \"使用自定义的消息数据格式\",\r\n  \"access.form.westcn_username.label\": \"西部数码代理商用户名\",\r\n  \"access.form.westcn_username.placeholder\": \"请输入西部数码代理商用户名\",\r\n  \"access.form.westcn_api_password.label\": \"西部数码代理商 API 密码\",\r\n  \"access.form.westcn_api_password.placeholder\": \"请输入西部数码代理商 API 密码\",\r\n  \"access.form.westcn_agent.guide\": \"西部数码 API 仅支持代理商调用。点击下方链接了解更多：<br><a href=\\\"https://www.west.cn/CustomerCenter/doc/apiv2.html#12u3001u8eabu4efdu9a8cu8bc10a3ca20id3d12u3001u8eabu4efdu9a8cu8bc13e203ca3e\\\" target=\\\"_blank\\\">https://www.west.cn/CustomerCenter/doc/apiv2.html</a>\",\r\n  \"access.form.xinnet_agent_id.label\": \"新网数码代理商编号\",\r\n  \"access.form.xinnet_agent_id.placeholder\": \"请输入新网数码代理商编号\",\r\n  \"access.form.xinnet_api_password.label\": \"新网数码代理商 API 密码\",\r\n  \"access.form.xinnet_api_password.placeholder\": \"请输入新网数码代理商 API 密码\",\r\n  \"access.form.xinnet_agent.guide\": \"新网数码 API 仅支持代理商调用。点击下方链接了解更多：<br><a href=\\\"https://apidoc.xin.cn/doc-7283837\\\" target=\\\"_blank\\\">https://apidoc.xin.cn/doc-7283837</a>\",\r\n  \"access.form.zerossl_eab.guide\": \"点击下方链接了解如何获取 ZeroSSL EAB：<br><a href=\\\"https://zerossl.com/documentation/acme/\\\" target=\\\"_blank\\\">https://zerossl.com/documentation/acme/</a>\"\r\n}\r\n"
  },
  {
    "path": "ui/src/i18n/locales/zh/nls.certificate.json",
    "content": "{\n  \"certificate.page.title\": \"SSL 证书\",\n  \"certificate.page.subtitle\": \"SSL 证书含有网站的公钥和网站标识以及其他相关信息。它们来自于工作流的运行输出。\",\n\n  \"certificate.nodata.title\": \"暂无证书\",\n  \"certificate.nodata.description\": \"当前未找到证书。请先运行一个工作流。\",\n  \"certificate.nodata.button\": \"前往工作流\",\n\n  \"certificate.search.placeholder\": \"按证书名称或序列号搜索……\",\n\n  \"certificate.action.view.menu\": \"查看详情\",\n  \"certificate.action.revoke.menu\": \"吊销证书\",\n  \"certificate.action.revoke.modal.title\": \"吊销「{{name}}」\",\n  \"certificate.action.revoke.modal.content\": \"确定要吊销该证书吗？<br>注意此操作不可撤销，请谨慎操作。\",\n  \"certificate.action.delete.menu\": \"删除证书\",\n  \"certificate.action.delete.modal.title\": \"删除「{{name}}」\",\n  \"certificate.action.delete.modal.content\": \"确定要删除该证书吗？<br>注意此操作不可撤销，请谨慎操作。\",\n  \"certificate.action.batch_delete.modal.title\": \"删除证书\",\n  \"certificate.action.batch_delete.modal.content\": \"确定要删除这 {{count}} 个被选中的证书吗？<br>注意此操作不可撤销，请谨慎操作。\",\n\n  \"certificate.props.subject_alt_names\": \"名称\",\n  \"certificate.props.validity\": \"有效期限\",\n  \"certificate.props.validity.left_days\": \"{{left}} / {{total}} 天\",\n  \"certificate.props.validity.less_than_a_day\": \"≤1 / {{total}} 天\",\n  \"certificate.props.validity.expired\": \"已过期\",\n  \"certificate.props.validity.expiration\": \"{{date}} 过期\",\n  \"certificate.props.validity.filter.all\": \"全部\",\n  \"certificate.props.validity.filter.expiring_soon\": \"即将过期\",\n  \"certificate.props.validity.filter.expired\": \"已过期\",\n  \"certificate.props.brand\": \"证书品牌\",\n  \"certificate.props.source\": \"来源\",\n  \"certificate.props.source.request\": \"申请\",\n  \"certificate.props.source.upload\": \"上传\",\n  \"certificate.props.revoked\": \"已吊销\",\n  \"certificate.props.certificate\": \"证书内容\",\n  \"certificate.props.private_key\": \"私钥内容\",\n  \"certificate.props.serial_number\": \"证书序列号\",\n  \"certificate.props.key_algorithm\": \"私钥算法\",\n  \"certificate.props.issuer\": \"颁发者\",\n  \"certificate.props.created_at\": \"创建时间\",\n  \"certificate.props.updated_at\": \"更新时间\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/zh/nls.common.json",
    "content": "﻿{\n  \"common.button.add\": \"新增\",\n  \"common.button.cancel\": \"取消\",\n  \"common.button.close\": \"关闭\",\n  \"common.button.confirm\": \"确定\",\n  \"common.button.copy\": \"复制\",\n  \"common.button.create\": \"新建\",\n  \"common.button.delete\": \"刪除\",\n  \"common.button.download\": \"下载\",\n  \"common.button.edit\": \"编辑\",\n  \"common.button.more\": \"更多\",\n  \"common.button.ok\": \"确定\",\n  \"common.button.reload\": \"重新加载\",\n  \"common.button.reset\": \"重置\",\n  \"common.button.save\": \"保存更改\",\n  \"common.button.save_and_continue\": \"保存并继续\",\n  \"common.button.submit\": \"提交\",\n  \"common.button.view\": \"查看\",\n\n  \"common.text.copied\": \"已复制到剪贴板\",\n  \"common.text.import_from_file\": \"从文件导入 ……\",\n  \"common.text.happy_browser\": \"当前浏览器版本过低，Certimate WebUI 无法正常工作。推荐使用 Google Chrome v119.0 或更高版本的现代浏览器。\",\n  \"common.text.nodata\": \"暂无数据\",\n  \"common.text.nodata_failed\": \"加载失败\",\n  \"common.text.operation_confirm\": \"操作确认\",\n  \"common.text.operation_succeeded\": \"操作成功\",\n  \"common.text.operation_failed\": \"操作失败\",\n  \"common.text.request_error\": \"请求错误\",\n  \"common.text.saved\": \"已保存\",\n  \"common.text.saving\": \"保存中 ……\",\n  \"common.text.search\": \"搜索 ……\",\n\n  \"common.menu.document\": \"文档\",\n  \"common.menu.theme\": \"切换主题\",\n  \"common.menu.locale\": \"切换语言\",\n  \"common.menu.gethelp\": \"获取帮助\",\n  \"common.menu.logout\": \"退出登录\",\n\n  \"common.theme.light\": \"浅色\",\n  \"common.theme.dark\": \"暗黑\",\n  \"common.theme.system\": \"自动\",\n\n  \"common.errmsg.string_max\": \"请输入不超过 {{max}} 个字符\",\n  \"common.errmsg.email_invalid\": \"请输入正确的邮箱\",\n  \"common.errmsg.domain_invalid\": \"请输入正确的域名\",\n  \"common.errmsg.host_invalid\": \"请输入正确的域名或 IP 地址\",\n  \"common.errmsg.port_invalid\": \"请输入正确的端口号\",\n  \"common.errmsg.ip_invalid\": \"请输入正确的 IP 地址\",\n  \"common.errmsg.url_invalid\": \"请输入正确的 URL 地址\",\n  \"common.errmsg.json_invalid\": \"请输入有效的 JSON 格式字符串\",\n  \"common.errmsg.form_invalid\": \"请检查表单内容\",\n\n  \"common.notifier.bark\": \"Bark\",\n  \"common.notifier.dingtalk\": \"钉钉\",\n  \"common.notifier.email\": \"邮件\",\n  \"common.notifier.gotify\": \"Gotify\",\n  \"common.notifier.lark\": \"飞书\",\n  \"common.notifier.mattermost\": \"Mattermost\",\n  \"common.notifier.pushover\": \"Pushover\",\n  \"common.notifier.pushplus\": \"PushPlus推送加\",\n  \"common.notifier.serverchan\": \"Server 酱\",\n  \"common.notifier.telegram\": \"Telegram\",\n  \"common.notifier.webhook\": \"Webhook\",\n  \"common.notifier.wecom\": \"企业微信\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/zh/nls.dashboard.json",
    "content": "﻿{\n  \"dashboard.page.title\": \"仪表盘\",\n\n  \"dashboard.statistics.all_certificates\": \"所有证书\",\n  \"dashboard.statistics.expiring_soon_certificates\": \"即将过期证书\",\n  \"dashboard.statistics.expired_certificates\": \"已过期证书\",\n  \"dashboard.statistics.all_workflows\": \"所有工作流\",\n  \"dashboard.statistics.enabled_workflows\": \"已启用工作流\",\n\n  \"dashboard.shortcut\": \"快捷操作\",\n  \"dashboard.shortcut.create_workflow\": \"创建新的工作流\",\n  \"dashboard.shortcut.change_account\": \"修改登录账号密码\",\n  \"dashboard.shortcut.configure_ca\": \"配置证书颁发机构\",\n  \"dashboard.shortcut.upgrade\": \"新版本可升级！\",\n\n  \"dashboard.recent_workflow_runs\": \"最近的工作流运行\",\n  \"dashboard.recent_workflow_runs.nodata.description\": \"当前未找到工作流运行历史。请先运行一个工作流。\",\n  \"dashboard.recent_workflow_runs.nodata.button\": \"前往工作流\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/zh/nls.login.json",
    "content": "﻿{\n  \"login.username.label\": \"用户名/邮箱\",\n  \"login.username.placeholder\": \"请输入用户名/邮箱\",\n  \"login.username.errmsg.invalid\": \"请输入正确的用户名/邮箱\",\n  \"login.password.label\": \"密码\",\n  \"login.password.placeholder\": \"请输入密码\",\n  \"login.password.errmsg.invalid\": \"密码至少 10 个字符\",\n  \"login.submit\": \"登录\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/zh/nls.preset.json",
    "content": "﻿{\n  \"preset.page.title\": \"预设模板\",\n  \"preset.page.subtitle\": \"预设模板代表一组可复用的数据片段，可以快速填充特定表单。\",\n\n  \"preset.action.create.button\": \"新建模板\",\n  \"preset.action.create.modal.title\": \"新建模板\",\n  \"preset.action.modify.menu\": \"编辑模板\",\n  \"preset.action.modify.modal.title\": \"编辑模板\",\n  \"preset.action.delete.menu\": \"删除模板\",\n  \"preset.action.delete.modal.title\": \"删除「{{name}}」\",\n  \"preset.action.delete.modal.content\": \"确定要删除该模板吗？<br>注意此操作不可撤销，请谨慎操作。\",\n\n  \"preset.props.name\": \"名称\",\n  \"preset.props.usage.notification\": \"通知模板\",\n  \"preset.props.usage.notification.tips\": \"你可以在工作流的通知节点中使用这些预设的通知主题和内容。\",\n  \"preset.props.usage.script\": \"脚本模板\",\n  \"preset.props.usage.script.tips\": \"你可以在工作流的部署节点（如本地主机、SSH 远程主机等）中使用这些预设的脚本命令。\",\n\n  \"preset.warning.excceeded\": \"可创建的模板数量已达上限\",\n  \"preset.form.name.label\": \"模板名称\",\n  \"preset.form.name.placeholder\": \"请输入模板名称\",\n  \"preset.form.name.errmsg.duplicated\": \"该名称已存在，请使用其他名称。\",\n  \"preset.form.notification_subject.label\": \"通知主题\",\n  \"preset.form.notification_subject.placeholder\": \"请输入通知主题\",\n  \"preset.form.notification_message.label\": \"通知内容\",\n  \"preset.form.notification_message.placeholder\": \"请输入通知内容\",\n  \"preset.form.script_command.label\": \"脚本命令\",\n  \"preset.form.script_command.placeholder\": \"请输入脚本命令\",\n\n  \"preset.dropdown.notification.button\": \"使用预设通知\",\n  \"preset.dropdown.script.button\": \"使用预设脚本\",\n  \"preset.dropdown.option_group.builtin\": \"内置模板\",\n  \"preset.dropdown.option_group.custom\": \"自定义模板\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/zh/nls.provider.json",
    "content": "{\r\n  \"provider.1panel\": \"1Panel\",\r\n  \"provider.1panel_console\": \"1Panel - 面板自身\",\r\n  \"provider.35cn\": \"三五互联\",\r\n  \"provider.51dnscom\": \"帝恩思（51DNS）\",\r\n  \"provider.acmeca\": \"自定义 ACME CA 端点\",\r\n  \"provider.acmedns\": \"ACME-DNS\",\r\n  \"provider.acmehttpreq\": \"自定义基于 HTTP 请求的 ACME 质询验证端点\",\r\n  \"provider.actalisssl\": \"Actalis SSL\",\r\n  \"provider.akamai\": \"Akamai\",\r\n  \"provider.akamai_cdn\": \"Akamai - 内容分发网络 CDN\",\r\n  \"provider.akamai_edgedns\": \"Akamai - EdgeDNS\",\r\n  \"provider.aliyun\": \"阿里云\",\r\n  \"provider.aliyun_alb\": \"阿里云 - 应用型负载均衡 ALB\",\r\n  \"provider.aliyun_apigw\": \"阿里云 - API 网关\",\r\n  \"provider.aliyun_cas_deploy\": \"阿里云 - 通过数字证书管理服务 CAS 创建部署任务\",\r\n  \"provider.aliyun_cas_upload\": \"阿里云 - 上传到数字证书管理服务 CAS\",\r\n  \"provider.aliyun_cdn\": \"阿里云 - 内容分发网络 CDN\",\r\n  \"provider.aliyun_clb\": \"阿里云 - 传统型负载均衡 CLB\",\r\n  \"provider.aliyun_dcdn\": \"阿里云 - 全站加速 DCDN\",\r\n  \"provider.aliyun_ddospro\": \"阿里云 - DDoS 高防\",\r\n  \"provider.aliyun_dns\": \"阿里云 - 云解析 DNS\",\r\n  \"provider.aliyun_esa\": \"阿里云 - 边缘安全加速 ESA\",\r\n  \"provider.aliyun_esa_saas\": \"阿里云 - 边缘安全加速 ESA SaaS 管理器\",\r\n  \"provider.aliyun_fc\": \"阿里云 - 函数计算 FC\",\r\n  \"provider.aliyun_ga\": \"阿里云 - 全球加速 GA\",\r\n  \"provider.aliyun_live\": \"阿里云 - 视频直播 Live\",\r\n  \"provider.aliyun_nlb\": \"阿里云 - 网络型负载均衡 NLB\",\r\n  \"provider.aliyun_oss\": \"阿里云 - 对象存储 OSS\",\r\n  \"provider.aliyun_vod\": \"阿里云 - 视频点播 VOD\",\r\n  \"provider.aliyun_waf\": \"阿里云 - Web 应用防火墙 WAF\",\r\n  \"provider.apisix\": \"Apache APISIX\",\r\n  \"provider.arvancloud\": \"ArvanCloud\",\r\n  \"provider.aws\": \"AWS\",\r\n  \"provider.aws_acm\": \"AWS - ACM (Amazon Certificate Manager)\",\r\n  \"provider.aws_cloudfront\": \"AWS - CloudFront\",\r\n  \"provider.aws_iam\": \"AWS - IAM (Identity and Access Management)\",\r\n  \"provider.aws_route53\": \"AWS - Route53\",\r\n  \"provider.azure\": \"Azure\",\r\n  \"provider.azure_dns\": \"Azure - DNS\",\r\n  \"provider.azure_keyvault\": \"Azure - KeyVault\",\r\n  \"provider.baiducloud\": \"百度智能云\",\r\n  \"provider.baiducloud_appblb\": \"百度智能云 - 应用型负载均衡 BLB\",\r\n  \"provider.baiducloud_blb\": \"百度智能云 - 普通型负载均衡 BLB\",\r\n  \"provider.baiducloud_cdn\": \"百度智能云 - 内容分发网络 CDN\",\r\n  \"provider.baiducloud_cert_upload\": \"百度智能云 - 上传到 SSL 证书服务\",\r\n  \"provider.baiducloud_dns\": \"百度智能云 - 智能云解析 DNS\",\r\n  \"provider.baishan\": \"白山云\",\r\n  \"provider.baishan_cdn\": \"白山云 - 内容分发网络 CDN\",\r\n  \"provider.baotapanel\": \"宝塔面板（又名：aaPanel）\",\r\n  \"provider.baotapanel_common\": \"宝塔面板\",\r\n  \"provider.baotapanel_console\": \"宝塔面板 - 面板自身\",\r\n  \"provider.baotapanelgo\": \"宝塔面板极速版（又名：aaPanel WinGo）\",\r\n  \"provider.baotapanelgo_common\": \"宝塔面板极速版\",\r\n  \"provider.baotapanelgo_console\": \"宝塔面板极速版 - 面板自身\",\r\n  \"provider.baotawaf\": \"堡塔云 WAF（又名：aaWAF）\",\r\n  \"provider.baotawaf_common\": \"堡塔云 WAF\",\r\n  \"provider.baotawaf_console\": \"堡塔云 WAF - 面板自身\",\r\n  \"provider.bookmyname\": \"BookMyName\",\r\n  \"provider.bunny\": \"Bunny\",\r\n  \"provider.bunny_cdn\": \"Bunny - 内容分发网络 CDN\",\r\n  \"provider.byteplus\": \"BytePlus\",\r\n  \"provider.byteplus_cdn\": \"BytePlus - 内容分发网络 CDN\",\r\n  \"provider.cachefly\": \"CacheFly\",\r\n  \"provider.cdnfly\": \"Cdnfly\",\r\n  \"provider.cloudflare\": \"Cloudflare\",\r\n  \"provider.cloudns\": \"ClouDNS\",\r\n  \"provider.cmcccloud\": \"移动云\",\r\n  \"provider.cmcccloud_dns\": \"移动云 - 云解析 DNS\",\r\n  \"provider.constellix\": \"Constellix\",\r\n  \"provider.cpanel\": \"cPanel\",\r\n  \"provider.ctcccloud\": \"天翼云\",\r\n  \"provider.ctcccloud_ao\": \"天翼云 - 边缘安全加速平台 AccessOne\",\r\n  \"provider.ctcccloud_cdn\": \"天翼云 - 内容分发网络 CDN\",\r\n  \"provider.ctcccloud_cms_upload\": \"天翼云 - 上传到证书管理服务 CMS\",\r\n  \"provider.ctcccloud_elb\": \"天翼云 - 弹性负载均衡 ELB\",\r\n  \"provider.ctcccloud_faas\": \"天翼云 - 函数计算 FaaS\",\r\n  \"provider.ctcccloud_icdn\": \"天翼云 - 全站加速 ICDN\",\r\n  \"provider.ctcccloud_lvdn\": \"天翼云 - 视频直播 LVDN\",\r\n  \"provider.ctcccloud_smartdns\": \"天翼云 - 智能 DNS\",\r\n  \"provider.cucccloud\": \"联通云\",\r\n  \"provider.desec\": \"deSEC\",\r\n  \"provider.digicert\": \"DigiCert\",\r\n  \"provider.digitalocean\": \"DigitalOcean\",\r\n  \"provider.dingtalkbot\": \"钉钉群机器人\",\r\n  \"provider.discordbot\": \"Discord 机器人\",\r\n  \"provider.dnsexit\": \"DNSExit\",\r\n  \"provider.dnsla\": \"帝恩爱斯（DNS.LA）\",\r\n  \"provider.dnsmadeeasy\": \"DNS Made Easy\",\r\n  \"provider.dogecloud\": \"多吉云\",\r\n  \"provider.dogecloud_cdn\": \"多吉云 - 内容分发网络 CDN\",\r\n  \"provider.dokploy\": \"Dokploy\",\r\n  \"provider.duckdns\": \"Duck DNS\",\r\n  \"provider.dynu\": \"Dynu\",\r\n  \"provider.dynv6\": \"dynv6\",\r\n  \"provider.email\": \"邮件（SMTP）\",\r\n  \"provider.fastly\": \"Fastly\",\r\n  \"provider.flexcdn\": \"FlexCDN\",\r\n  \"provider.flyio\": \"Fly.io\",\r\n  \"provider.gandinet\": \"Gandi.net\",\r\n  \"provider.gcore\": \"G-Core\",\r\n  \"provider.gcore_cdn\": \"G-Core - 内容分发网络 CDN\",\r\n  \"provider.globalsignatlas\": \"GlobalSign Atlas\",\r\n  \"provider.gname\": \"GNAME\",\r\n  \"provider.godaddy\": \"GoDaddy\",\r\n  \"provider.goedge\": \"GoEdge\",\r\n  \"provider.googletrustservices\": \"Google Trust Services\",\r\n  \"provider.hetzner\": \"Hetzner\",\r\n  \"provider.hostingde\": \"hosting.de\",\r\n  \"provider.hostinger\": \"Hostinger\",\r\n  \"provider.huaweicloud\": \"华为云\",\r\n  \"provider.huaweicloud_cdn\": \"华为云 - 内容分发网络 CDN\",\r\n  \"provider.huaweicloud_dns\": \"华为云 - 云解析 DNS\",\r\n  \"provider.huaweicloud_elb\": \"华为云 - 弹性负载均衡 ELB\",\r\n  \"provider.huaweicloud_obs\": \"华为云 - 对象储存服务 OBS\",\r\n  \"provider.huaweicloud_scm_upload\": \"华为云 - 上传到云证书管理服务 SCM\",\r\n  \"provider.huaweicloud_waf\": \"华为云 - Web 应用防火墙 WAF\",\r\n  \"provider.infomaniak\": \"Infomaniak\",\r\n  \"provider.ionos\": \"IONOS\",\r\n  \"provider.jdcloud\": \"京东云\",\r\n  \"provider.jdcloud_alb\": \"京东云 - 应用负载均衡 ALB\",\r\n  \"provider.jdcloud_cdn\": \"京东云 - 内容分发网络 CDN\",\r\n  \"provider.jdcloud_dns\": \"京东云 - 云解析 DNS\",\r\n  \"provider.jdcloud_live\": \"京东云 - 视频直播\",\r\n  \"provider.jdcloud_vod\": \"京东云 - 视频点播\",\r\n  \"provider.kong\": \"Kong\",\r\n  \"provider.kubernetes\": \"Kubernetes\",\r\n  \"provider.kubernetes_secret\": \"Kubernetes - Secret\",\r\n  \"provider.ksyun\": \"金山云\",\r\n  \"provider.ksyun_cdn\": \"金山云 - 内容分发网络 CDN\",\r\n  \"provider.larkbot\": \"飞书群机器人\",\r\n  \"provider.lecdn\": \"LeCDN\",\r\n  \"provider.letsencrypt\": \"Let's Encrypt\",\r\n  \"provider.letsencryptstaging\": \"Let's Encrypt 测试环境\",\r\n  \"provider.linode\": \"Linode\",\r\n  \"provider.litessl\": \"LiteSSL\",\r\n  \"provider.local\": \"本地主机\",\r\n  \"provider.mattermost\": \"Mattermost\",\r\n  \"provider.mohua\": \"嘿华云\",\r\n  \"provider.mohua_mvh\": \"嘿华云 - 虚拟主机 MVH \",\r\n  \"provider.namecheap\": \"Namecheap\",\r\n  \"provider.namedotcom\": \"Name.com\",\r\n  \"provider.namesilo\": \"NameSilo\",\r\n  \"provider.netcup\": \"netcup\",\r\n  \"provider.netlify\": \"Netlify\",\r\n  \"provider.nginxproxymanager\": \"Nginx Proxy Manager\",\r\n  \"provider.ns1\": \"NS1 (IBM NS1 Connect)\",\r\n  \"provider.ovhcloud\": \"OVHcloud\",\r\n  \"provider.porkbun\": \"Porkbun\",\r\n  \"provider.powerdns\": \"PowerDNS\",\r\n  \"provider.proxmoxve\": \"Proxmox VE\",\r\n  \"provider.qingcloud\": \"青云\",\r\n  \"provider.qingcloud_dns\": \"青云 - 云解析 DNS\",\r\n  \"provider.qiniu\": \"七牛云\",\r\n  \"provider.qiniu_cdn\": \"七牛云 - 内容分发网络 CDN\",\r\n  \"provider.qiniu_kodo\": \"七牛云 - 对象存储 Kodo\",\r\n  \"provider.qiniu_pili\": \"七牛云 - 视频直播 Pili\",\r\n  \"provider.rainyun\": \"雨云\",\r\n  \"provider.rainyun_rcdn\": \"雨云 - 雨盾 CDN\",\r\n  \"provider.rainyun_sslcenter_upload\": \"雨云 - 上传到 SSL 证书中心\",\r\n  \"provider.ratpanel\": \"耗子面板（又名：AcePanel）\",\r\n  \"provider.ratpanel_common\": \"耗子面板\",\r\n  \"provider.ratpanel_console\": \"耗子面板 - 面板自身\",\r\n  \"provider.rfc2136\": \"RFC 2136: Dynamic DNS Updates\",\r\n  \"provider.s3\": \"对象存储（S3 兼容）\",\r\n  \"provider.s3_upload\": \"上传到 S3 兼容的对象存储\",\r\n  \"provider.safeline\": \"雷池\",\r\n  \"provider.sectigo\": \"Sectigo\",\r\n  \"provider.slackbot\": \"Slack 机器人\",\r\n  \"provider.spaceship\": \"Spaceship\",\r\n  \"provider.ssh\": \"远程主机（SSH）\",\r\n  \"provider.sslcom\": \"SSL.com\",\r\n  \"provider.synologydsm\": \"群晖 DSM\",\r\n  \"provider.technitiumdns\": \"Technitium DNS\",\r\n  \"provider.telegrambot\": \"Telegram 机器人\",\r\n  \"provider.tencentcloud\": \"腾讯云\",\r\n  \"provider.tencentcloud_cdn\": \"腾讯云 - 内容分发网络 CDN\",\r\n  \"provider.tencentcloud_clb\": \"腾讯云 - 负载均衡 CLB\",\r\n  \"provider.tencentcloud_cos\": \"腾讯云 - 对象存储 COS\",\r\n  \"provider.tencentcloud_css\": \"腾讯云 - 云直播 CSS\",\r\n  \"provider.tencentcloud_dns\": \"腾讯云 - 云解析 DNS\",\r\n  \"provider.tencentcloud_ecdn\": \"腾讯云 - 全站加速网络 ECDN\",\r\n  \"provider.tencentcloud_eo\": \"腾讯云 - 边缘安全加速平台 EdgeOne\",\r\n  \"provider.tencentcloud_gaap\": \"腾讯云 - 全球应用加速 GAAP\",\r\n  \"provider.tencentcloud_scf\": \"腾讯云 - 云函数 SCF\",\r\n  \"provider.tencentcloud_ssl_deploy\": \"腾讯云 - 通过 SSL 证书服务创建部署任务\",\r\n  \"provider.tencentcloud_ssl_update\": \"腾讯云 - 通过 SSL 证书服务更新云资源证书\",\r\n  \"provider.tencentcloud_ssl_upload\": \"腾讯云 - 上传到 SSL 证书服务\",\r\n  \"provider.tencentcloud_vod\": \"腾讯云 - 云点播 VOD\",\r\n  \"provider.tencentcloud_waf\": \"腾讯云 - Web 应用防火墙 WAF\",\r\n  \"provider.todaynic\": \"时代互联\",\r\n  \"provider.ucloud\": \"优刻得\",\r\n  \"provider.ucloud_ualb\": \"优刻得 - 应用型负载均衡 ALB\",\r\n  \"provider.ucloud_ucdn\": \"优刻得 - 云分发 UCDN\",\r\n  \"provider.ucloud_uclb\": \"优刻得 - 传统型负载均衡 CLB\",\r\n  \"provider.ucloud_udnr\": \"优刻得 - 域名服务 UDNR\",\r\n  \"provider.ucloud_uewaf\": \"优刻得 - 企业 Web 应用防火墙 UEWAF\",\r\n  \"provider.ucloud_upathx\": \"优刻得 - 全球动态加速 UPathX\",\r\n  \"provider.ucloud_us3\": \"优刻得 - 对象存储 US3\",\r\n  \"provider.unicloud\": \"uniCloud (DCloud)\",\r\n  \"provider.unicloud_webhost\": \"uniCloud - 前端网页托管\",\r\n  \"provider.upyun\": \"又拍云\",\r\n  \"provider.upyun_cdn\": \"又拍云 - 云分发 CDN\",\r\n  \"provider.upyun_file\": \"又拍云 - 云存储 USS\",\r\n  \"provider.vercel\": \"Vercel\",\r\n  \"provider.volcengine\": \"火山引擎\",\r\n  \"provider.volcengine_alb\": \"火山引擎 - 应用型负载均衡 ALB\",\r\n  \"provider.volcengine_cdn\": \"火山引擎 - 内容分发网络 CDN\",\r\n  \"provider.volcengine_certcenter_upload\": \"火山引擎 - 上传到证书中心\",\r\n  \"provider.volcengine_clb\": \"火山引擎 - 负载均衡 CLB\",\r\n  \"provider.volcengine_dcdn\": \"火山引擎 - 全站加速 DCDN\",\r\n  \"provider.volcengine_dns\": \"火山引擎 - 云解析 DNS\",\r\n  \"provider.volcengine_imagex\": \"火山引擎 - 图片服务 ImageX\",\r\n  \"provider.volcengine_live\": \"火山引擎 - 视频直播 Live\",\r\n  \"provider.volcengine_tos\": \"火山引擎 - 对象存储 TOS\",\r\n  \"provider.volcengine_vod\": \"火山引擎 - 视频点播 VOD\",\r\n  \"provider.volcengine_waf\": \"火山引擎 - Web 应用防火墙 WAF\",\r\n  \"provider.vultr\": \"Vultr\",\r\n  \"provider.wangsu\": \"网宿云\",\r\n  \"provider.wangsu_cdn\": \"网宿云 - 内容分发网络 CDN\",\r\n  \"provider.wangsu_cdnpro\": \"网宿云 - CDN Pro (CDN 360)\",\r\n  \"provider.wangsu_certificate_upload\": \"网宿云 - 上传到证书管理\",\r\n  \"provider.webhook\": \"Webhook\",\r\n  \"provider.wecombot\": \"企业微信群机器人\",\r\n  \"provider.westcn\": \"西部数码\",\r\n  \"provider.xinnet\": \"新网数码\",\r\n  \"provider.zerossl\": \"ZeroSSL\",\r\n\r\n  \"provider.category.all\": \"全部\",\r\n  \"provider.category.cdn\": \"CDN\",\r\n  \"provider.category.storage\": \"文件存储\",\r\n  \"provider.category.loadbalance\": \"负载均衡\",\r\n  \"provider.category.firewall\": \"防火墙\",\r\n  \"provider.category.av\": \"音视频\",\r\n  \"provider.category.accelerator\": \"加速器\",\r\n  \"provider.category.apigw\": \"API 网关\",\r\n  \"provider.category.serverless\": \"Serverless\",\r\n  \"provider.category.website\": \"网站托管\",\r\n  \"provider.category.ssl\": \"证书托管\",\r\n  \"provider.category.other\": \"其他\",\r\n\r\n  \"provider.text.nodata\": \"暂无提供商\",\r\n  \"provider.text.default_ca\": \"（默认）不指定，跟随全局设置\",\r\n  \"provider.text.default_ca_in_group\": \"不指定，跟随全局设置\",\r\n  \"provider.text.default_group\": \"默认\",\r\n  \"provider.text.available_group\": \"可用（已添加授权凭据）\",\r\n  \"provider.text.unavailable_group\": \"不可用（未添加授权凭据）\",\r\n  \"provider.text.unavailable_divider\": \"以下提供商不可用（即未添加过授权凭据）\"\r\n}\r\n"
  },
  {
    "path": "ui/src/i18n/locales/zh/nls.settings.json",
    "content": "﻿{\n  \"settings.page.title\": \"系统设置\",\n\n  \"settings.account.tab\": \"账号\",\n  \"settings.account.username.title\": \"登录用户名\",\n  \"settings.account.username.tips\": \"使用电子邮箱登录你的账号。\",\n  \"settings.account.username.button.label\": \"修改邮箱\",\n  \"settings.account.username.form.email.label\": \"邮箱\",\n  \"settings.account.username.form.email.placeholder\": \"请输入邮箱\",\n  \"settings.account.password.title\": \"登录密码\",\n  \"settings.account.password.tips\": \"建议定期修改密码，以保护你的数据安全。\",\n  \"settings.account.password.button.label\": \"修改密码\",\n  \"settings.account.password.form.email.old_password.label\": \"当前密码\",\n  \"settings.account.password.form.email.old_password.placeholder\": \"请输入旧密码\",\n  \"settings.account.password.form.email.new_password.label\": \"新密码\",\n  \"settings.account.password.form.email.new_password.placeholder\": \"请输入新密码\",\n  \"settings.account.password.form.email.confirm_password.label\": \"确认密码\",\n  \"settings.account.password.form.email.confirm_password.placeholder\": \"请再次输入新密码\",\n  \"settings.account.password.form.email.password.errmsg.invalid\": \"密码至少 10 个字符\",\n  \"settings.account.password.form.email.password.errmsg.not_matched\": \"两次密码不一致\",\n  \"settings.account.2fa.title\": \"双因子认证（2FA）\",\n\n  \"settings.appearance.tab\": \"外观\",\n  \"settings.appearance.theme.title\": \"主题\",\n  \"settings.appearance.theme.form.value.extra\": \"重新加载页面生效。\",\n  \"settings.appearance.language.title\": \"语言\",\n  \"settings.appearance.language.form.value.extra\": \"重新加载页面生效。\",\n  \"settings.appearance.pagination.title\": \"分页\",\n  \"settings.appearance.pagination.form.default_per_page.label\": \"列表页默认显示数量\",\n  \"settings.appearance.pagination.form.default_per_page.placeholder\": \"请输入列表页默认显示数量\",\n  \"settings.appearance.pagination.form.default_per_page.unit\": \"条每页\",\n  \"settings.appearance.workflow.title\": \"工作流\",\n  \"settings.appearance.workflow.form.default_designer_layout.label\": \"编辑器默认布局\",\n  \"settings.appearance.workflow.form.default_designer_layout.placeholder\": \"请选择编辑器默认布局\",\n  \"settings.appearance.workflow.form.default_designer_layout.option.horizontal\": \"水平布局\",\n  \"settings.appearance.workflow.form.default_designer_layout.option.vertical\": \"垂直布局\",\n\n  \"settings.sslprovider.tab\": \"证书颁发机构\",\n  \"settings.sslprovider.ca.title\": \"全局证书颁发机构\",\n  \"settings.sslprovider.ca.tips\": \"如果你希望在每个工作流中选择不同的证书颁发机构，请前往授权凭据页面。\",\n  \"settings.sslprovider.ca.form.provider.label\": \"ACME 提供商\",\n  \"settings.sslprovider.ca.form.provider.help\": \"注意：不同服务商所支持的证书有效期、私钥算法、多域名数量上限、是否允许泛域名等可能不同，切换服务商后请注意检查已有工作流的配置是否需要调整。\",\n  \"settings.sslprovider.ca.form.letsencryptstaging_alert\": \"测试环境比生产环境有更宽松的速率限制，可进行测试性部署。<br><br>点击下方链接了解更多：<br><a href=\\\"https://letsencrypt.org/zh-cn/docs/staging-environment/\\\" target=\\\"_blank\\\">https://letsencrypt.org/zh-cn/docs/staging-environment/</a>\",\n  \"settings.sslprovider.others.title\": \"其他参数\",\n  \"settings.sslprovider.others.form.timeout.label\": \"证书订单超时时间\",\n  \"settings.sslprovider.others.form.timeout.placeholder\": \"请输入证书订单超时时间\",\n  \"settings.sslprovider.others.form.timeout.unit\": \"秒\",\n  \"settings.sslprovider.others.form.timeout.tooltip\": \"表示等待证书订单就绪的超时时间。如果你不了解该选项的用途，保持默认即可。\",\n\n  \"settings.persistence.tab\": \"数据持久化\",\n  \"settings.persistence.alerting.title\": \"警报策略\",\n  \"settings.persistence.alerting.form.certificates_warning_days_before_expire.label\": \"证书即将过期预警阈值\",\n  \"settings.persistence.alerting.form.certificates_warning_days_before_expire.placeholder\": \"请输入证书即将过期预警阈值\",\n  \"settings.persistence.alerting.form.certificates_warning_days_before_expire.unit\": \"天\",\n  \"settings.persistence.alerting.form.certificates_warning_days_before_expire.help\": \"提示：该选项将决定将证书过期前多久视为「即将过期」。\",\n  \"settings.persistence.data_retention.title\": \"数据保留策略\",\n  \"settings.persistence.data_retention.form.workflow_runs_retention_max_days.label\": \"工作流运行历史保留期限\",\n  \"settings.persistence.data_retention.form.workflow_runs_retention_max_days.placeholder\": \"请输入运行历史保留期限\",\n  \"settings.persistence.data_retention.form.workflow_runs_retention_max_days.unit\": \"天\",\n  \"settings.persistence.data_retention.form.workflow_runs_retention_max_days.help\": \"提示：设置为 <b>0</b> 表示永久保留，不会自动清理。建议设置为 <b>180</b> 天以上。\",\n  \"settings.persistence.data_retention.form.certificates_retention_max_days.label\": \"证书过期后保留期限\",\n  \"settings.persistence.data_retention.form.certificates_retention_max_days.placeholder\": \"请输入过期证书保留期限\",\n  \"settings.persistence.data_retention.form.certificates_retention_max_days.unit\": \"天\",\n  \"settings.persistence.data_retention.form.certificates_retention_max_days.help\": \"提示：设置为 <b>0</b> 表示永久保留，不会自动清理。\",\n\n  \"settings.diagnostics.tab\": \"系统诊断\",\n  \"settings.diagnostics.logs.title\": \"系统日志\",\n  \"settings.diagnostics.logs.button.refresh.label\": \"刷新日志\",\n  \"settings.diagnostics.logs.button.load_more.label\": \"加载更多\",\n  \"settings.diagnostics.crons.title\": \"后台任务\",\n  \"settings.diagnostics.crons.props.next_trigger_time\": \"预计下次运行时间：\",\n  \"settings.diagnostics.workflow_dispatcher.title\": \"工作流调度器\",\n  \"settings.diagnostics.workflow_dispatcher.statistics.concurrency\": \"最大并发\",\n  \"settings.diagnostics.workflow_dispatcher.statistics.pending\": \"等待运行\",\n  \"settings.diagnostics.workflow_dispatcher.statistics.processing\": \"运行中\",\n\n  \"settings.about.tab\": \"关于\",\n  \"settings.about.version.new\": \"新版本可升级\",\n  \"settings.about.socials.document\": \"文档\",\n  \"settings.about.socials.github\": \"GitHub\",\n  \"settings.about.socials.telegram\": \"Telegram\",\n  \"settings.about.socials.donate\": \"捐赠\",\n  \"settings.about.feedback.title\": \"帮助我们改进 Certimate\",\n  \"settings.about.feedback.subtitle\": \"告诉我们如何让 Certimate 为你更好地服务。\",\n  \"settings.about.feedback.button\": \"意见反馈\",\n  \"settings.about.contributors.title\": \"贡献者\",\n  \"settings.about.contributors.tips\": \"感谢所有贡献者对本项目做出的贡献。\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/zh/nls.workflow.json",
    "content": "{\n  \"workflow.page.title\": \"工作流\",\n  \"workflow.page.subtitle\": \"工作流是自动化流程的节点集合。当满足触发条件时，工作流开始顺序运行各节点以完成复杂的任务。\",\n\n  \"workflow.nodata.title\": \"暂无工作流\",\n  \"workflow.nodata.description\": \"当前未找到工作流。请先创建。\",\n  \"workflow.nodata.button\": \"新建工作流\",\n\n  \"workflow.search.placeholder\": \"按工作流名称搜索……\",\n\n  \"workflow.action.create.button\": \"新建工作流\",\n  \"workflow.action.create.modal.title\": \"新建工作流\",\n  \"workflow.action.modify.menu\": \"编辑工作流\",\n  \"workflow.action.duplicate.menu\": \"复制工作流\",\n  \"workflow.action.delete.menu\": \"删除工作流\",\n  \"workflow.action.delete.modal.title\": \"删除「{{name}}」\",\n  \"workflow.action.delete.modal.content\": \"确定要删除该工作流吗？<br>注意此操作不可撤销，请谨慎操作。\",\n  \"workflow.action.batch_delete.modal.title\": \"删除工作流\",\n  \"workflow.action.batch_delete.modal.content\": \"确定要删除这 {{count}} 个被选中的工作流吗？<br>注意此操作不可撤销，请谨慎操作。\",\n  \"workflow.action.enable.button\": \"启用\",\n  \"workflow.action.enable.errmsg.unpublished\": \"请先完成流程编排并发布更改\",\n  \"workflow.action.disable.button\": \"停用\",\n  \"workflow.action.execute.button\": \"运行\",\n  \"workflow.action.execute.menu\": \"运行工作流\",\n  \"workflow.action.execute.modal.title\": \"运行工作流\",\n  \"workflow.action.execute.modal.content\": \"你有尚未发布的更改。确定要以最近一次发布的版本继续运行吗？\",\n  \"workflow.action.execute.prompt\": \"运行中……请稍后查看运行历史\",\n\n  \"workflow.props.name\": \"名称\",\n  \"workflow.props.description\": \"描述\",\n  \"workflow.props.trigger\": \"触发方式\",\n  \"workflow.props.trigger.scheduled\": \"定时\",\n  \"workflow.props.trigger.manual\": \"手动\",\n  \"workflow.props.last_run_at\": \"最近运行时间\",\n  \"workflow.props.state\": \"是否启用\",\n  \"workflow.props.state.filter.all\": \"全部\",\n  \"workflow.props.state.filter.enabled\": \"启用\",\n  \"workflow.props.state.filter.disabled\": \"未启用\",\n  \"workflow.props.created_at\": \"创建时间\",\n  \"workflow.props.updated_at\": \"更新时间\",\n\n  \"workflow.new.title\": \"新建工作流\",\n  \"workflow.new.subtitle\": \"使用工作流来监控域名、申请证书、部署上传和发送通知。\",\n  \"workflow.new.button.create\": \"创建空白工作流\",\n  \"workflow.new.button.import\": \"从文件导入……\",\n  \"workflow.new.templates.title\": \"选择模板\",\n  \"workflow.new.templates.subtitle\": \"使用这些模板快速初始化你的自动化证书管理工作流。\",\n  \"workflow.new.templates.template.standard.title\": \"标准业务流程\",\n  \"workflow.new.templates.template.standard.description\": \"一个包含证书申请 + 证书部署 + 消息通知步骤的工作流程，可适用于绝大多数业务场景。\",\n  \"workflow.new.templates.template.certtest.title\": \"域名证书监控\",\n  \"workflow.new.templates.template.certtest.description\": \"一个包含证书监控 + 消息通知步骤的工作流程，可在线上证书到期前或已过期时发出告警。\",\n  \"workflow.new.templates.default_name\": \"未命名工作流\",\n  \"workflow.new.templates.default_description\": \"\",\n\n  \"workflow.detail.baseinfo.name.label\": \"工作流名称\",\n  \"workflow.detail.baseinfo.name.placeholder\": \"请输入工作流名称\",\n  \"workflow.detail.baseinfo.description.label\": \"工作流描述\",\n  \"workflow.detail.baseinfo.description.placeholder\": \"请输入工作流描述\",\n  \"workflow.detail.design.tab\": \"流程编排\",\n  \"workflow.detail.design.editor.placeholder\": \"请完成配置\",\n  \"workflow.detail.design.editor.add_node\": \"添加节点\",\n  \"workflow.detail.design.editor.rename_node\": \"重命名\",\n  \"workflow.detail.design.editor.duplicate_node\": \"复制节点\",\n  \"workflow.detail.design.editor.remove_node\": \"删除节点\",\n  \"workflow.detail.design.editor.add_branch\": \"添加分支\",\n  \"workflow.detail.design.editor.rename_branch\": \"重命名\",\n  \"workflow.detail.design.editor.duplicate_branch\": \"复制分支\",\n  \"workflow.detail.design.editor.remove_branch\": \"删除分支\",\n  \"workflow.detail.design.toolbar.zoomin\": \"放大\",\n  \"workflow.detail.design.toolbar.zoomout\": \"缩小\",\n  \"workflow.detail.design.toolbar.auto_fit\": \"自适应视图\",\n  \"workflow.detail.design.toolbar.horizontal_layout\": \"横向布局\",\n  \"workflow.detail.design.toolbar.vertical_layout\": \"竖向布局\",\n  \"workflow.detail.design.toolbar.minimap\": \"小地图\",\n  \"workflow.detail.design.toolbar.drag_mode\": \"触摸模式\",\n  \"workflow.detail.design.toolbar.pointer_mode\": \"指针模式\",\n  \"workflow.detail.design.drawer.node_id.label\": \"节点 ID：\",\n  \"workflow.detail.design.drawer.disabled.on.tooltip\": \"禁用\",\n  \"workflow.detail.design.drawer.disabled.off.tooltip\": \"启用\",\n  \"workflow.detail.design.action.publish.button\": \"发布更改\",\n  \"workflow.detail.design.action.publish.modal.title\": \"发布更改\",\n  \"workflow.detail.design.action.publish.modal.content\": \"确定要发布更改吗？\",\n  \"workflow.detail.design.action.rollback.button\": \"回退更改\",\n  \"workflow.detail.design.action.rollback.modal.title\": \"回退更改\",\n  \"workflow.detail.design.action.rollback.modal.content\": \"确定要回退到最近一次发布的版本吗？\",\n  \"workflow.detail.design.action.import.button\": \"导入\",\n  \"workflow.detail.design.action.import.modal.title\": \"导入工作流\",\n  \"workflow.detail.design.action.import.modal.ok_button\": \"导入\",\n  \"workflow.detail.design.action.import.form.format.label\": \"格式\",\n  \"workflow.detail.design.action.import.form.content.label\": \"内容\",\n  \"workflow.detail.design.action.import.form.content.errmsg.invalid\": \"请输入有效的内容\",\n  \"workflow.detail.design.action.import.form.content.errmsg.first_node_start\": \"第一个节点必须是开始节点\",\n  \"workflow.detail.design.action.import.form.content.errmsg.last_node_end\": \"最后一个节点必须是结束节点\",\n  \"workflow.detail.design.action.import.form.content.errmsg.duplicate_start\": \"有且只能有一个开始节点\",\n  \"workflow.detail.design.action.import.form.content.errmsg.invalid_id\": \"节点 ID 无效：#{{nodeId}}\",\n  \"workflow.detail.design.action.import.form.content.errmsg.invalid_config\": \"节点配置无效：#{{nodeId}}\",\n  \"workflow.detail.design.action.import.form.content.errmsg.conflict_id\": \"节点 ID 冲突：#{{nodeId}}\",\n  \"workflow.detail.design.action.import.form.content.errmsg.abnormal_condition_branch\": \"并行/条件分支结构异常：#{{nodeId}}\",\n  \"workflow.detail.design.action.import.form.content.errmsg.abnormal_try_catch_branch\": \"执行结果分支结构异常：#{{nodeId}}\",\n  \"workflow.detail.design.action.export.button\": \"导出\",\n  \"workflow.detail.design.action.export.modal.title\": \"导出工作流\",\n  \"workflow.detail.design.action.export.form.format.label\": \"格式\",\n  \"workflow.detail.design.action.export.form.content.label\": \"内容\",\n  \"workflow.detail.design.uncompleted_design.alert\": \"流程编排未就绪，请检查是否有节点未完成配置。\",\n  \"workflow.detail.design.unpublished_draft.alert\": \"当前编排有尚未发布的更改。\",\n  \"workflow.detail.design.unsaved_changes.confirm\": \"你有尚未保存的更改。确定要关闭面板吗？\",\n  \"workflow.detail.runs.tab\": \"运行历史\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/zh/nls.workflow.nodes.json",
    "content": "{\r\n  \"workflow_node.kind.basis\": \"基础\",\r\n  \"workflow_node.kind.business\": \"业务\",\r\n  \"workflow_node.kind.logic\": \"逻辑\",\r\n\r\n  \"workflow_node.start.label\": \"开始\",\r\n  \"workflow_node.start.default_name\": \"开始\",\r\n  \"workflow_node.start.form_anchor.parameters.tab\": \"参数设置\",\r\n  \"workflow_node.start.form.trigger.label\": \"触发方式\",\r\n  \"workflow_node.start.form.trigger.placeholder\": \"请选择触发方式\",\r\n  \"workflow_node.start.form.trigger.option.scheduled.label\": \"定时触发\",\r\n  \"workflow_node.start.form.trigger.option.manual.label\": \"手动触发\",\r\n  \"workflow_node.start.form.trigger_cron.label\": \"CRON 表达式\",\r\n  \"workflow_node.start.form.trigger_cron.placeholder\": \"请输入 CRON 表达式\",\r\n  \"workflow_node.start.form.trigger_cron.errmsg.invalid\": \"请输入正确的 CRON 表达式\",\r\n  \"workflow_node.start.form.trigger_cron.tooltip\": \"五段式表达式，使用 <em>crontab</em> 标准语法规则。<br>支持使用任意值（即 <strong>*</strong>）、值列表分隔符（即 <strong>,</strong>）、值的范围（即 <strong>-</strong>）、步骤值（即 <strong>/</strong>）等四种表达式。\",\r\n  \"workflow_node.start.form.trigger_cron.help\": \"预计最近 5 次运行时间（实际时区以服务器设置为准）：\",\r\n  \"workflow_node.start.form.trigger_cron.guide\": \"如果你有多个工作流，建议将它们设置为在一天中的多个时间段运行，而非总是在相同的特定时间。也不要总是设置为每日零时，以免遭遇证书颁发机构的流量高峰。<br><br>参考链接：<br>1. <a href=\\\"https://letsencrypt.org/zh-cn/docs/rate-limits/\\\" target=\\\"_blank\\\">Let’s Encrypt 速率限制</a><br>2. <a href=\\\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\\\" target=\\\"_blank\\\">为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机？</a>\",\r\n\r\n  \"workflow_node.apply.label\": \"申请签发证书\",\r\n  \"workflow_node.apply.default_name\": \"申请\",\r\n  \"workflow_node.apply.form_anchor.parameters.tab\": \"参数设置\",\r\n  \"workflow_node.apply.form_anchor.challenge.tab\": \"验证质询\",\r\n  \"workflow_node.apply.form_anchor.challenge.title\": \"验证质询\",\r\n  \"workflow_node.apply.form_anchor.certificate.tab\": \"证书设置\",\r\n  \"workflow_node.apply.form_anchor.certificate.title\": \"证书设置\",\r\n  \"workflow_node.apply.form_anchor.advanced.tab\": \"高级设置\",\r\n  \"workflow_node.apply.form_anchor.advanced.title\": \"高级设置\",\r\n  \"workflow_node.apply.form_anchor.strategy.tab\": \"执行策略\",\r\n  \"workflow_node.apply.form_anchor.strategy.title\": \"执行策略\",\r\n  \"workflow_node.apply.form.identifier.label\": \"证书标识\",\r\n  \"workflow_node.apply.form.identifier.label2\": \"请选择证书标识类型：\",\r\n  \"workflow_node.apply.form.identifier.option.domain.label\": \"域名证书\",\r\n  \"workflow_node.apply.form.identifier.option.domain.description\": \"<ul style=\\\"margin: 0;\\\"><li>支持单域名、多域名、泛域名（取决于具体证书颁发机构）</li><li>支持 DNS-01、HTTP-01 质询验证</li></ul>\",\r\n  \"workflow_node.apply.form.identifier.option.ip.label\": \"IP 地址证书\",\r\n  \"workflow_node.apply.form.identifier.option.ip.description\": \"<ul style=\\\"margin: 0;\\\"><li>支持 IPv4、IPv6 地址</li><li>仅支持 HTTP-01 质询验证</li><li>预设配置：无证书通用名称（即 CommonName）</li><li>预设配置：证书颁发机构 <em><b>Let's Encrypt</b></em></li><li>预设配置：ACME 配置文件 <em><b>shortlived</b></em></li><li>更短的证书有效期和续期间隔</li></ul>\",\r\n  \"workflow_node.apply.form.identifier.continue.button\": \"下一步\",\r\n  \"workflow_node.apply.form.domains.label\": \"域名\",\r\n  \"workflow_node.apply.form.domains.placeholder\": \"请输入域名（多个值请用半角分号隔开）\",\r\n  \"workflow_node.apply.form.domains.help\": \"提示：多域名请用半角分号隔开；泛域名表示形式为 <em>*.example.com</em>。\",\r\n  \"workflow_node.apply.form.domains.help_no_wildcard\": \"提示：支持多个域名，以半角分号隔开。\",\r\n  \"workflow_node.apply.form.domains.multiple_input_modal.title\": \"修改域名\",\r\n  \"workflow_node.apply.form.domains.multiple_input_modal.placeholder\": \"请输入域名\",\r\n  \"workflow_node.apply.form.ipaddrs.label\": \"IP 地址\",\r\n  \"workflow_node.apply.form.ipaddrs.placeholder\": \"请输入 IP 地址（多个值请用半角分号隔开）\",\r\n  \"workflow_node.apply.form.ipaddrs.help\": \"提示：多 IP 地址请用半角分号隔开。\",\r\n  \"workflow_node.apply.form.ipaddrs.multiple_input_modal.title\": \"修改 IP 地址\",\r\n  \"workflow_node.apply.form.ipaddrs.multiple_input_modal.placeholder\": \"请输入 IP 地址\",\r\n  \"workflow_node.apply.form.contact_email.label\": \"联系邮箱\",\r\n  \"workflow_node.apply.form.contact_email.placeholder\": \"请输入联系邮箱\",\r\n  \"workflow_node.apply.form.contact_email.tooltip\": \"申请签发 SSL 证书时所需的联系方式。请注意 Let's Encrypt 账户注册的<a href=\\\"https://letsencrypt.org/zh-cn/docs/rate-limits/\\\" target=\\\"_blank\\\">速率限制</a>。\",\r\n  \"workflow_node.apply.form.challenge_type.label\": \"质询方式\",\r\n  \"workflow_node.apply.form.challenge_type.placeholder\": \"请选择质询方式\",\r\n  \"workflow_node.apply.form.challenge_type.errmsg.no_wildcard_in_http01\": \"HTTP-01 质询无法用于申请泛域名证书。\",\r\n  \"workflow_node.apply.form.challenge_type.errmsg.no_ip_in_dns01\": \"DNS-01 质询无法用于申请 IP 地址证书。\",\r\n  \"workflow_node.apply.form.challenge_type.tooltip\": \"表示证书颁发机构如何验证你对域名的控制权。<br><a href=\\\"https://letsencrypt.org/zh-cn/docs/challenge-types/\\\" target=\\\"_blank\\\">点此了解更多</a>。\",\r\n  \"workflow_node.apply.form.provider.label\": \"提供商\",\r\n  \"workflow_node.apply.form.provider.placeholder\": \"请选择提供商\",\r\n  \"workflow_node.apply.form.provider_dns01.label\": \"DNS 提供商\",\r\n  \"workflow_node.apply.form.provider_dns01.placeholder\": \"请选择 DNS 提供商\",\r\n  \"workflow_node.apply.form.provider_http01.label\": \"主机提供商\",\r\n  \"workflow_node.apply.form.provider_http01.placeholder\": \"请选择主机提供商\",\r\n  \"workflow_node.apply.form.provider_access.label\": \"提供商授权\",\r\n  \"workflow_node.apply.form.provider_access.placeholder\": \"请选择提供商授权\",\r\n  \"workflow_node.apply.form.provider_access.button\": \"新建\",\r\n  \"workflow_node.apply.form.provider_access_dns01.label\": \"DNS 提供商授权\",\r\n  \"workflow_node.apply.form.provider_access_dns01.placeholder\": \"请选择 DNS 提供商授权\",\r\n  \"workflow_node.apply.form.provider_access_http01.label\": \"主机提供商授权\",\r\n  \"workflow_node.apply.form.provider_access_http01.placeholder\": \"请选择主机提供商授权\",\r\n  \"workflow_node.apply.form.aliyun_esa_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.apply.form.aliyun_esa_region.placeholder\": \"请输入阿里云 ESA 服务地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.apply.form.aliyun_esa_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint</a>\",\r\n  \"workflow_node.apply.form.aws_route53_region.label\": \"AWS 服务区域\",\r\n  \"workflow_node.apply.form.aws_route53_region.placeholder\": \"请输入 AWS Route53 服务区域（例如：us-east-1）\",\r\n  \"workflow_node.apply.form.aws_route53_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints</a>\",\r\n  \"workflow_node.apply.form.aws_route53_hosted_zone_id.label\": \"AWS Route53 托管区域 ID（可选）\",\r\n  \"workflow_node.apply.form.aws_route53_hosted_zone_id.placeholder\": \"请输入 AWS Route53 托管区域 ID\",\r\n  \"workflow_node.apply.form.aws_route53_hosted_zone_id.help\": \"提示：仅当存在多个相同 FQDN 的托管区域时需要填写。\",\r\n  \"workflow_node.apply.form.aws_route53_hosted_zone_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.aws.amazon.com/zh_cn/Route53/latest/DeveloperGuide/hosted-zones-working-with.html\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/zh_cn/Route53/latest/DeveloperGuide/hosted-zones-working-with.html</a>\",\r\n  \"workflow_node.apply.form.huaweicloud_dns_region.label\": \"华为云服务区域\",\r\n  \"workflow_node.apply.form.huaweicloud_dns_region.placeholder\": \"请输入华为云 DNS 服务区域（例如：cn-north-1）\",\r\n  \"workflow_node.apply.form.huaweicloud_dns_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.huaweicloud.com/apiexplorer/#/endpoint\\\" target=\\\"_blank\\\">https://console.huaweicloud.com/apiexplorer/#/endpoint</a>\",\r\n  \"workflow_node.apply.form.jdcloud_dns_region_id.label\": \"京东云服务地域 ID\",\r\n  \"workflow_node.apply.form.jdcloud_dns_region_id.placeholder\": \"请输入京东云 DNS 服务地域 ID（例如：cn-north-1）\",\r\n  \"workflow_node.apply.form.jdcloud_dns_region_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.jdcloud.com/cn/common-declaration/api/introduction\\\" target=\\\"_blank\\\">https://docs.jdcloud.com/cn/common-declaration/api/introduction</a>\",\r\n  \"workflow_node.apply.form.s3_region.label\": \"对象存储区域\",\r\n  \"workflow_node.apply.form.s3_region.placeholder\": \"请输入对象存储区域\",\r\n  \"workflow_node.apply.form.s3_bucket.label\": \"对象存储桶名\",\r\n  \"workflow_node.apply.form.s3_bucket.placeholder\": \"请输入对象存储桶名\",\r\n  \"workflow_node.apply.form.local_webroot_path.label\": \"网站根目录\",\r\n  \"workflow_node.apply.form.local_webroot_path.placeholder\": \"请输入网站根目录\",\r\n  \"workflow_node.apply.form.local_webroot_path.tooltip\": \"即服务器上存储网站文件的主文件夹。\",\r\n  \"workflow_node.apply.form.ssh_webroot_path.label\": \"网站根目录\",\r\n  \"workflow_node.apply.form.ssh_webroot_path.placeholder\": \"请输入网站根目录\",\r\n  \"workflow_node.apply.form.ssh_webroot_path.tooltip\": \"即服务器上存储网站文件的主文件夹。\",\r\n  \"workflow_node.apply.form.ssh_use_scp.label\": \"回退使用 SCP\",\r\n  \"workflow_node.apply.form.ssh_use_scp.tooltip\": \"如果你的远程服务器不支持 SFTP，请勾选此选项回退为 SCP。\",\r\n  \"workflow_node.apply.form.key_source.label\": \"私钥来源\",\r\n  \"workflow_node.apply.form.key_source.placeholder\": \"请选择私钥来源\",\r\n  \"workflow_node.apply.form.key_source.option.auto.label\": \"随机生成\",\r\n  \"workflow_node.apply.form.key_source.option.reuse.label\": \"复用私钥\",\r\n  \"workflow_node.apply.form.key_source.option.custom.label\": \"自定义\",\r\n  \"workflow_node.apply.form.key_algorithm.label\": \"私钥算法\",\r\n  \"workflow_node.apply.form.key_algorithm.placeholder\": \"请选择证书的私钥算法\",\r\n  \"workflow_node.apply.form.key_algorithm.help_reuse\": \"提示：如果存在之前申请的证书，将以原私钥算法为准；否则才使用此选项。\",\r\n  \"workflow_node.apply.form.key_algorithm.help_custom\": \"注意：请确保算法与私钥相匹配。\",\r\n  \"workflow_node.apply.form.key_content.label\": \"私钥文件（PEM 格式）\",\r\n  \"workflow_node.apply.form.key_content.placeholder\": \"-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----\",\r\n  \"workflow_node.apply.form.key_content.errmsg.invalid\": \"请输入有效的 PEM 格式私钥文件\",\r\n  \"workflow_node.apply.form.key_content.errmsg.not_matched\": \"私钥内容与算法不匹配（预期：{{expected}}，实际：{{actual}}）\",\r\n  \"workflow_node.apply.form.ca_provider.label\": \"证书颁发机构（可选）\",\r\n  \"workflow_node.apply.form.ca_provider.placeholder\": \"请选择证书颁发机构\",\r\n  \"workflow_node.apply.form.ca_provider.button\": \"设置\",\r\n  \"workflow_node.apply.form.ca_provider_access.label\": \"证书颁发机构授权\",\r\n  \"workflow_node.apply.form.ca_provider_access.placeholder\": \"请选择证书颁发机构授权\",\r\n  \"workflow_node.apply.form.ca_provider_access.button\": \"新建\",\r\n  \"workflow_node.apply.form.validity_lifetime.label\": \"有效期（可选）\",\r\n  \"workflow_node.apply.form.validity_lifetime.placeholder\": \"请输入证书的有效期\",\r\n  \"workflow_node.apply.form.validity_lifetime.help\": \"注意：并非所有证书颁发机构都支持此特性。\",\r\n  \"workflow_node.apply.form.validity_lifetime.tooltip\": \"表示证书的有效期。如果你不了解该选项的用途，保持默认即可。\",\r\n  \"workflow_node.apply.form.validity_lifetime.units.h\": \"小时\",\r\n  \"workflow_node.apply.form.validity_lifetime.units.d\": \"天\",\r\n  \"workflow_node.apply.form.preferred_chain.label\": \"首选链（可选）\",\r\n  \"workflow_node.apply.form.preferred_chain.placeholder\": \"请输入证书首选链\",\r\n  \"workflow_node.apply.form.preferred_chain.help\": \"注意：并非所有证书颁发机构都支持此特性。\",\r\n  \"workflow_node.apply.form.preferred_chain.tooltip\": \"表示证书颁发时使用的首选证书链。如果你不了解该选项的用途，保持默认即可。<br><a href=\\\"https://letsencrypt.org/zh-cn/certificates/\\\" target=\\\"_blank\\\">点此了解更多</a>。\",\r\n  \"workflow_node.apply.form.acme_profile.label\": \"ACME 配置文件（可选）\",\r\n  \"workflow_node.apply.form.acme_profile.placeholder\": \"请输入 ACME 配置文件\",\r\n  \"workflow_node.apply.form.acme_profile.help\": \"注意：并非所有证书颁发机构都支持此特性。\",\r\n  \"workflow_node.apply.form.acme_profile.tooltip\": \"表示证书颁发时使用的 ACME 证书配置文件。如果你不了解该选项的用途，保持默认即可。<br><a href=\\\"https://letsencrypt.org/zh-cn/docs/profiles/#%E6%88%91%E4%BB%AC%E6%8F%90%E4%BE%9B%E7%9A%84%E9%85%8D%E7%BD%AE\\\" target=\\\"_blank\\\">点此了解更多</a>。\",\r\n  \"workflow_node.apply.form.disable_cn.label\": \"阻止 CSR 包含主题通用名称\",\r\n  \"workflow_node.apply.form.disable_cn.tooltip\": \"在证书中是否包含主题通用名称字段（即 <em>Subject.CommonName</em>）。如果你不了解该选项的用途，保持默认即可。<br><a href=\\\"https://letsencrypt.org/zh-cn/docs/profiles/#%E8%AF%81%E4%B9%A6%E9%80%9A%E7%94%A8%E5%90%8D%E7%A7%B0\\\" target=\\\"_blank\\\">点此了解更多</a>。\",\r\n  \"workflow_node.apply.form.nameservers.label\": \"DNS 递归服务器（可选）\",\r\n  \"workflow_node.apply.form.nameservers.placeholder\": \"请输入 DNS 递归服务器（多个值请用半角分号隔开）\",\r\n  \"workflow_node.apply.form.nameservers.tooltip\": \"表示在 ACME 质询时使用自定义的 DNS 递归服务器。如果你不了解该选项的用途，保持默认即可。<br><a href=\\\"https://go-acme.github.io/lego/usage/cli/options/index.html#dns-resolvers-and-challenge-verification\\\" target=\\\"_blank\\\">点此了解更多</a>。\",\r\n  \"workflow_node.apply.form.nameservers.multiple_input_modal.title\": \"修改 DNS 递归服务器\",\r\n  \"workflow_node.apply.form.nameservers.multiple_input_modal.placeholder\": \"请输入 DNS 递归服务器\",\r\n  \"workflow_node.apply.form.dns_propagation_wait.label\": \"DNS 传播等待时间（可选）\",\r\n  \"workflow_node.apply.form.dns_propagation_wait.placeholder\": \"请输入 DNS 传播等待时间\",\r\n  \"workflow_node.apply.form.dns_propagation_wait.unit\": \"秒\",\r\n  \"workflow_node.apply.form.dns_propagation_wait.tooltip\": \"表示在 ACME DNS-01 质询时 DNS 传播的等待时间。如果你不了解此选项的用途，保持默认即可。\",\r\n  \"workflow_node.apply.form.dns_propagation_timeout.label\": \"DNS 传播检查超时时间（可选）\",\r\n  \"workflow_node.apply.form.dns_propagation_timeout.placeholder\": \"请输入 DNS 传播检查超时时间\",\r\n  \"workflow_node.apply.form.dns_propagation_timeout.unit\": \"秒\",\r\n  \"workflow_node.apply.form.dns_propagation_timeout.tooltip\": \"表示在 ACME DNS-01 质询时 DNS 传播检查的超时时间。如果你不了解此选项的用途，保持默认即可。\",\r\n  \"workflow_node.apply.form.dns_ttl.label\": \"DNS 解析记录 TTL（可选）\",\r\n  \"workflow_node.apply.form.dns_ttl.placeholder\": \"请输入 DNS 解析记录 TTL\",\r\n  \"workflow_node.apply.form.dns_ttl.unit\": \"秒\",\r\n  \"workflow_node.apply.form.dns_ttl.help\": \"提示：不填写时，将使用 DNS 提供商指定的默认值。\",\r\n  \"workflow_node.apply.form.dns_ttl.tooltip\": \"表示在 ACME DNS-01 质询时 DNS 解析记录的 TTL。如果你不了解此选项的用途，保持默认即可。\",\r\n  \"workflow_node.apply.form.http_delay_wait.label\": \"HTTP 服务器等待时间（可选）\",\r\n  \"workflow_node.apply.form.http_delay_wait.placeholder\": \"请输入 HTTP 服务器等待时间\",\r\n  \"workflow_node.apply.form.http_delay_wait.unit\": \"秒\",\r\n  \"workflow_node.apply.form.http_delay_wait.tooltip\": \"表示在 ACME HTTP-01 质询时的 HTTP 服务器等待时间。如果你不了解此选项的用途，保持默认即可。\",\r\n  \"workflow_node.apply.form.disable_follow_cname.label\": \"阻止 CNAME 跟随\",\r\n  \"workflow_node.apply.form.disable_follow_cname.tooltip\": \"在 ACME DNS-01 质询时是否阻止 CNAME 跟随。如果你不了解该选项的用途，保持默认即可。<a href=\\\"https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme/#the-advantages-of-a-cname\\\" target=\\\"_blank\\\">点此了解更多</a>。\",\r\n  \"workflow_node.apply.form.disable_ari.label\": \"阻止 ARI 续期\",\r\n  \"workflow_node.apply.form.disable_ari.tooltip\": \"在 ACME 证书续期时是否阻止 ARI（ACME Renewal Information）。如果你不了解该选项的用途，保持默认即可。<br><a href=\\\"https://letsencrypt.org/2023/03/23/improving-resliiency-and-reliability-with-ari/\\\" target=\\\"_blank\\\">点此了解更多</a>。\",\r\n  \"workflow_node.apply.form.skip_before_expiry_days.label\": \"重复申请\",\r\n  \"workflow_node.apply.form.skip_before_expiry_days.placeholder\": \"请输入续期间隔\",\r\n  \"workflow_node.apply.form.skip_before_expiry_days.prefix\": \"当上次申请证书成功后、且证书剩余有效期大于\",\r\n  \"workflow_node.apply.form.skip_before_expiry_days.suffix\": \"，再次运行工作流时跳过此申请节点。\",\r\n  \"workflow_node.apply.form.skip_before_expiry_days.unit\": \"天\",\r\n\r\n  \"workflow_node.upload.label\": \"上传自有证书\",\r\n  \"workflow_node.upload.default_name\": \"上传\",\r\n  \"workflow_node.upload.form_anchor.parameters.tab\": \"参数设置\",\r\n  \"workflow_node.upload.form.guide\": \"每次执行此节点时，都将重新读取文件内容。\",\r\n  \"workflow_node.upload.form.source.label\": \"上传来源\",\r\n  \"workflow_node.upload.form.source.placeholder\": \"请选择上传来源\",\r\n  \"workflow_node.upload.form.source.option.form.label\": \"表单\",\r\n  \"workflow_node.upload.form.source.option.local.label\": \"本地路径\",\r\n  \"workflow_node.upload.form.source.option.url.label\": \"URL 路径\",\r\n  \"workflow_node.upload.form.name.label\": \"证书名称\",\r\n  \"workflow_node.upload.form.name.placeholder\": \"上传证书文件后显示\",\r\n  \"workflow_node.upload.form.certificate_pem.label\": \"证书文件（PEM 格式）\",\r\n  \"workflow_node.upload.form.certificate_pem.placeholder\": \"-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----\",\r\n  \"workflow_node.upload.form.certificate_pem.errmsg.invalid\": \"请输入有效的 PEM 格式证书文件\",\r\n  \"workflow_node.upload.form.certificate_path.label\": \"证书文件路径\",\r\n  \"workflow_node.upload.form.certificate_path.placeholder\": \"请输入证书文件本地路径\",\r\n  \"workflow_node.upload.form.certificate_url.label\": \"证书文件 URL\",\r\n  \"workflow_node.upload.form.certificate_url.placeholder\": \"请输入证书文件下载 URL\",\r\n  \"workflow_node.upload.form.private_key_pem.label\": \"私钥文件（PEM 格式）\",\r\n  \"workflow_node.upload.form.private_key_pem.placeholder\": \"-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----\",\r\n  \"workflow_node.upload.form.private_key_pem.errmsg.invalid\": \"请输入有效的 PEM 格式私钥文件\",\r\n  \"workflow_node.upload.form.private_key_path.label\": \"私钥文件路径\",\r\n  \"workflow_node.upload.form.private_key_path.placeholder\": \"请输入私钥文件本地路径\",\r\n  \"workflow_node.upload.form.private_key_url.label\": \"私钥文件 URL\",\r\n  \"workflow_node.upload.form.private_key_url.placeholder\": \"请输入私钥文件下载 URL\",\r\n\r\n  \"workflow_node.monitor.label\": \"监控网站证书\",\r\n  \"workflow_node.monitor.default_name\": \"监控\",\r\n  \"workflow_node.monitor.form_anchor.parameters.tab\": \"参数设置\",\r\n  \"workflow_node.monitor.form.guide\": \"将向目标地址发送一个 HEAD 请求来获取相应的域名证书，请确保该地址可通过 HTTPS 协议访问。\",\r\n  \"workflow_node.monitor.form.host.label\": \"主机地址\",\r\n  \"workflow_node.monitor.form.host.placeholder\": \"请输入主机地址（域名或 IP）\",\r\n  \"workflow_node.monitor.form.port.label\": \"主机端口\",\r\n  \"workflow_node.monitor.form.port.placeholder\": \"请输入主机端口\",\r\n  \"workflow_node.monitor.form.domain.label\": \"域名（可选）\",\r\n  \"workflow_node.monitor.form.domain.placeholder\": \"请输入域名\",\r\n  \"workflow_node.monitor.form.domain.help\": \"提示：仅当主机地址为 IP 时需要填写。\",\r\n  \"workflow_node.monitor.form.request_path.label\": \"请求路径（可选）\",\r\n  \"workflow_node.monitor.form.request_path.placeholder\": \"请输入请求路径\",\r\n\r\n  \"workflow_node.deploy.label\": \"部署证书到 ...\",\r\n  \"workflow_node.deploy.default_name\": \"部署\",\r\n  \"workflow_node.deploy.form_anchor.parameters.tab\": \"参数设置\",\r\n  \"workflow_node.deploy.form_anchor.deployment.tab\": \"部署设置\",\r\n  \"workflow_node.deploy.form_anchor.deployment.title\": \"部署设置\",\r\n  \"workflow_node.deploy.form_anchor.strategy.tab\": \"执行策略\",\r\n  \"workflow_node.deploy.form_anchor.strategy.title\": \"执行策略\",\r\n  \"workflow_node.deploy.form.certificate_output_node_id.label\": \"待部署证书\",\r\n  \"workflow_node.deploy.form.certificate_output_node_id.placeholder\": \"请选择待部署证书\",\r\n  \"workflow_node.deploy.form.certificate_output_node_id.help\": \"提示：待部署证书来自之前的申请或上传节点，如果选项为空请先检查前序节点。\",\r\n  \"workflow_node.deploy.form.provider.label\": \"部署目标\",\r\n  \"workflow_node.deploy.form.provider.placeholder\": \"请选择部署目标\",\r\n  \"workflow_node.deploy.form.provider.search.placeholder\": \"搜索部署目标……\",\r\n  \"workflow_node.deploy.form.provider_access.label\": \"主机提供商授权\",\r\n  \"workflow_node.deploy.form.provider_access.placeholder\": \"请选择主机提供商授权\",\r\n  \"workflow_node.deploy.form.provider_access.button\": \"新建\",\r\n  \"workflow_node.deploy.form.shared_resource_type.label\": \"证书部署方式\",\r\n  \"workflow_node.deploy.form.shared_resource_type.placeholder\": \"请选择证书部署方式\",\r\n  \"workflow_node.deploy.form.shared_domain_match_pattern.label\": \"域名匹配模式\",\r\n  \"workflow_node.deploy.form.shared_domain_match_pattern.placeholder\": \"请选择部署域名匹配模式\",\r\n  \"workflow_node.deploy.form.shared_domain_match_pattern.option.exact.label\": \"精确匹配\",\r\n  \"workflow_node.deploy.form.shared_domain_match_pattern.option.wildcard.label\": \"通配符匹配（泛域名）\",\r\n  \"workflow_node.deploy.form.shared_domain_match_pattern.option.certsan.label\": \"根据证书自动匹配\",\r\n  \"workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard\": \"注意：对于支持泛解析的站点，<strong>精确匹配</strong>一个泛域名仅包含该站点本身、不包括相关子域名站点。\",\r\n  \"workflow_node.deploy.form.shared_script_command.vartips\": \"支持的变量：<br><ol style=\\\"list-style: disc;\\\"><li><strong>${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}</strong>：<br>证书文件路径，等同于表单中相应字段的值。</li><li><strong>${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}</strong>：<br>证书文件（仅含服务器证书）路径，等同于表单中相应字段的值。</li><li><strong>${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_INTERMEDIA_PATH}</strong>：<br>证书文件（仅含中间证书）路径，等同于表单中相应字段的值。</li><li><strong>${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}</strong>：<br>私钥文件路径，等同于表单中相应字段的值。</li><li><strong>${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}</strong>：<br>PFX 导出密码，等同于表单中相应字段的值。</li><li><strong>${CERTIMATE_DEPLOYER_CMDVAR_JKS_ALIAS}</strong>：<br>JKS 别名，等同于表单中相应字段的值。</li><li><strong>${CERTIMATE_DEPLOYER_CMDVAR_JKS_KEYPASS}</strong>：<br>JKS 私钥访问口令，等同于表单中相应字段的值。</li><li><strong>${CERTIMATE_DEPLOYER_CMDVAR_JKS_STOREPASS}</strong>：<br>JKS 密钥库存储口令，等同于表单中相应字段的值。</li></ol>\",\r\n  \"workflow_node.deploy.form.1panel_node_name.label\": \"1Panel 子节点名称（可选）\",\r\n  \"workflow_node.deploy.form.1panel_node_name.placeholder\": \"请输入 1Panel 子节点名称\",\r\n  \"workflow_node.deploy.form.1panel_node_name.help\": \"提示：仅 1Panel v2+ 需要填写。不填写时，将替换主控节点证书；否则，将替换被控节点证书。\",\r\n  \"workflow_node.deploy.form.1panel_node_name.tooltip\": \"请登录 1Panel 面板查看\",\r\n  \"workflow_node.deploy.form.1panel_resource_type.option.website.label\": \"部署到指定网站\",\r\n  \"workflow_node.deploy.form.1panel_resource_type.option.certificate.label\": \"替换指定证书\",\r\n  \"workflow_node.deploy.form.1panel_website_match_pattern.label\": \"网站匹配模式\",\r\n  \"workflow_node.deploy.form.1panel_website_match_pattern.placeholder\": \"请选择部署网站匹配模式\",\r\n  \"workflow_node.deploy.form.1panel_website_match_pattern.option.specified.label\": \"指定 ID\",\r\n  \"workflow_node.deploy.form.1panel_website_match_pattern.option.certsan.label\": \"根据证书自动匹配\",\r\n  \"workflow_node.deploy.form.1panel_website_match_pattern.help_certsan\": \"注意：网站名称需要为域名、且包含开启了 SSL 的域名配置。\",\r\n  \"workflow_node.deploy.form.1panel_website_id.label\": \"1Panel 网站 ID\",\r\n  \"workflow_node.deploy.form.1panel_website_id.placeholder\": \"请输入 1Panel 网站 ID\",\r\n  \"workflow_node.deploy.form.1panel_website_id.tooltip\": \"请登录 1Panel 面板查看\",\r\n  \"workflow_node.deploy.form.1panel_certificate_id.label\": \"1Panel 证书 ID\",\r\n  \"workflow_node.deploy.form.1panel_certificate_id.placeholder\": \"请输入 1Panel 证书 ID\",\r\n  \"workflow_node.deploy.form.1panel_certificate_id.tooltip\": \"请登录 1Panel 面板查看\",\r\n  \"workflow_node.deploy.form.1panel_console_auto_restart.label\": \"部署后自动重启 1Panel 服务\",\r\n  \"workflow_node.deploy.form.aliyun_alb_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.deploy.form.aliyun_alb_region.placeholder\": \"请输入阿里云 ALB 服务地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.deploy.form.aliyun_alb_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/slb/application-load-balancer/product-overview/supported-regions-and-zones\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/slb/application-load-balancer/product-overview/supported-regions-and-zones</a>\",\r\n  \"workflow_node.deploy.form.aliyun_alb_resource_type.option.loadbalancer.label\": \"部署到指定负载均衡器下的全部 HTTPS/QUIC 监听器\",\r\n  \"workflow_node.deploy.form.aliyun_alb_resource_type.option.listener.label\": \"部署到指定 HTTPS/QUIC 监听器\",\r\n  \"workflow_node.deploy.form.aliyun_alb_loadbalancer_id.label\": \"阿里云 ALB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.aliyun_alb_loadbalancer_id.placeholder\": \"请输入阿里云 ALB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.aliyun_alb_loadbalancer_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://slb.console.aliyun.com/alb\\\" target=\\\"_blank\\\">https://slb.console.aliyun.com/alb</a>\",\r\n  \"workflow_node.deploy.form.aliyun_alb_listener_id.label\": \"阿里云 ALB 监听器 ID\",\r\n  \"workflow_node.deploy.form.aliyun_alb_listener_id.placeholder\": \"请输入阿里云 ALB 监听器 ID\",\r\n  \"workflow_node.deploy.form.aliyun_alb_listener_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://slb.console.aliyun.com/alb\\\" target=\\\"_blank\\\">https://slb.console.aliyun.com/alb</a>\",\r\n  \"workflow_node.deploy.form.aliyun_alb_snidomain.label\": \"阿里云 ALB 扩展域名（可选）\",\r\n  \"workflow_node.deploy.form.aliyun_alb_snidomain.placeholder\": \"请输入阿里云 ALB 扩展域名\",\r\n  \"workflow_node.deploy.form.aliyun_alb_snidomain.help\": \"提示：不填写时，将替换监听器的默认证书；否则，将替换扩展域名证书。\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_region.placeholder\": \"请输入阿里云 API 网关地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/product-overview/regions\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/product-overview/regions</a>\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_service_type.label\": \"阿里云 API 网关服务类型\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_service_type.placeholder\": \"请选择阿里云 API 网关服务类型\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_service_type.option.cloudnative.label\": \"云原生 API 网关\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_service_type.option.traditional.label\": \"原 API 网关\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_gateway_id.label\": \"阿里云 API 网关 ID\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_gateway_id.placeholder\": \"请输入阿里云 API 网关 ID\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_gateway_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://apigw.console.aliyun.com\\\" target=\\\"_blank\\\">https://apigw.console.aliyun.com</a>\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_group_id.label\": \"阿里云 API 分组 ID\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_group_id.placeholder\": \"请输入阿里云 API 分组 ID\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_group_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://apigateway.console.aliyun.com\\\" target=\\\"_blank\\\">https://apigateway.console.aliyun.com</a>\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_domain.label\": \"阿里云 API 网关自定义域名\",\r\n  \"workflow_node.deploy.form.aliyun_apigw_domain.placeholder\": \"请输入阿里云 API 网关自定义域名\",\r\n  \"workflow_node.deploy.form.aliyun_cas_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.deploy.form.aliyun_cas_region.placeholder\": \"请输入阿里云 CAS 服务地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.deploy.form.aliyun_cas_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/ssl-certificate/developer-reference/endpoints\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/ssl-certificate/developer-reference/endpoints</a>\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy.guide\": \"将通过阿里云 OpenAPI <em>CreateDeploymentJob</em> 接口创建异步部署任务。此部署目标若执行成功仅代表已创建部署任务，实际部署结果需要你自行前往阿里云控制台查询。\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_region.placeholder\": \"请输入阿里云 CAS 服务地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/ssl-certificate/developer-reference/endpoints\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/ssl-certificate/developer-reference/endpoints</a>\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.label\": \"阿里云云产品资源 ID\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.placeholder\": \"请输入阿里云云产品资源 ID（多个值请用半角分号隔开）\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.errmsg.invalid\": \"请输入正确的阿里云云产品资源 ID\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.help\": \"提示：支持多个 ID，以半角分号隔开。仅支持阿里云产品，注意与各产品本身的实例 ID 区分。\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-listcloudresources\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-listcloudresources</a>\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.multiple_input_modal.title\": \"修改阿里云云产品资源 ID\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_resource_ids.multiple_input_modal.placeholder\": \"请输入阿里云云产品资源 ID\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.label\": \"阿里云联系人 ID（可选）\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.placeholder\": \"请输入阿里云联系人 ID（多个值请用半角分号隔开）\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.errmsg.invalid\": \"请输入正确的阿里云联系人 ID\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.help\": \"提示：支持多个 ID，以半角分号隔开。不填写时，将使用系统联系人列表中的第一个。\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-listcontact\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-listcontact</a>\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.multiple_input_modal.title\": \"修改阿里云联系人 ID\",\r\n  \"workflow_node.deploy.form.aliyun_casdeploy_contact_ids.multiple_input_modal.placeholder\": \"请输入阿里云联系人 ID\",\r\n  \"workflow_node.deploy.form.aliyun_clb_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.deploy.form.aliyun_clb_region.placeholder\": \"请输入阿里云 CLB 服务地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.deploy.form.aliyun_clb_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/slb/classic-load-balancer/product-overview/regions-that-support-clb\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/slb/classic-load-balancer/product-overview/regions-that-support-clb</a>\",\r\n  \"workflow_node.deploy.form.aliyun_clb_resource_type.option.loadbalancer.label\": \"部署到指定负载均衡器下的全部 HTTPS 监听\",\r\n  \"workflow_node.deploy.form.aliyun_clb_resource_type.option.listener.label\": \"部署到指定 HTTPS 监听\",\r\n  \"workflow_node.deploy.form.aliyun_clb_loadbalancer_id.label\": \"阿里云 CLB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.aliyun_clb_loadbalancer_id.placeholder\": \"请输入阿里云 CLB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.aliyun_clb_loadbalancer_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://slb.console.aliyun.com/clb\\\" target=\\\"_blank\\\">https://slb.console.aliyun.com/clb</a>\",\r\n  \"workflow_node.deploy.form.aliyun_clb_listener_port.label\": \"阿里云 CLB 监听端口\",\r\n  \"workflow_node.deploy.form.aliyun_clb_listener_port.placeholder\": \"请输入阿里云 CLB 监听端口\",\r\n  \"workflow_node.deploy.form.aliyun_clb_listener_port.tooltip\": \"这是什么？请参阅 <a href=\\\"https://slb.console.aliyun.com/clb\\\" target=\\\"_blank\\\">https://slb.console.aliyun.com/clb</a>\",\r\n  \"workflow_node.deploy.form.aliyun_clb_snidomain.label\": \"阿里云 CLB 扩展域名（可选）\",\r\n  \"workflow_node.deploy.form.aliyun_clb_snidomain.placeholder\": \"请输入阿里云 CLB 扩展域名\",\r\n  \"workflow_node.deploy.form.aliyun_clb_snidomain.help\": \"提示：不填写时，将替换监听器的默认证书；否则，将替换扩展域名证书。\",\r\n  \"workflow_node.deploy.form.aliyun_cdn_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.deploy.form.aliyun_cdn_region.placeholder\": \"请输入阿里云 CDN 服务地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.deploy.form.aliyun_cdn_region.tooltip\": \"中国站请填写 <strong>cn-hangzhou</strong>；<br>国际站请填写 <strong>ap-southeast-1</strong>。\",\r\n  \"workflow_node.deploy.form.aliyun_cdn_domain.label\": \"阿里云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.aliyun_cdn_domain.placeholder\": \"请输入阿里云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.aliyun_dcdn_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.deploy.form.aliyun_dcdn_region.placeholder\": \"请输入阿里云 DCDN 服务地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.deploy.form.aliyun_dcdn_region.tooltip\": \"中国站请填写 <strong>cn-hangzhou</strong>；<br>国际站请填写 <strong>ap-southeast-1</strong>。\",\r\n  \"workflow_node.deploy.form.aliyun_dcdn_domain.label\": \"阿里云 DCDN 加速域名\",\r\n  \"workflow_node.deploy.form.aliyun_dcdn_domain.placeholder\": \"请输入阿里云 DCDN 加速域名\",\r\n  \"workflow_node.deploy.form.aliyun_ddospro_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.deploy.form.aliyun_ddospro_region.placeholder\": \"请输入阿里云 DDoS 高防服务地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.deploy.form.aliyun_ddospro_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/anti-ddos/anti-ddos-pro-and-premium/developer-reference/api-ddoscoo-2020-01-01-endpoint\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/anti-ddos/anti-ddos-pro-and-premium/developer-reference/api-ddoscoo-2020-01-01-endpoint</a>\",\r\n  \"workflow_node.deploy.form.aliyun_ddospro_domain.label\": \"阿里云 DDoS 高防网站域名\",\r\n  \"workflow_node.deploy.form.aliyun_ddospro_domain.placeholder\": \"请输入阿里云 DDoS 高防网站域名\",\r\n  \"workflow_node.deploy.form.aliyun_esa_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.deploy.form.aliyun_esa_region.placeholder\": \"请输入阿里云 ESA 服务地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.deploy.form.aliyun_esa_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint</a>\",\r\n  \"workflow_node.deploy.form.aliyun_esa_site_id.label\": \"阿里云 ESA 站点 ID\",\r\n  \"workflow_node.deploy.form.aliyun_esa_site_id.placeholder\": \"请输入阿里云 ESA 站点 ID\",\r\n  \"workflow_node.deploy.form.aliyun_esa_site_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://esa.console.aliyun.com/siteManage/list\\\" target=\\\"_blank\\\">https://esa.console.aliyun.com/siteManage/list</a>\",\r\n  \"workflow_node.deploy.form.aliyun_esa_saas_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.deploy.form.aliyun_esa_saas_region.placeholder\": \"请输入阿里云 ESA 服务地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.deploy.form.aliyun_esa_saas_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint</a>\",\r\n  \"workflow_node.deploy.form.aliyun_esa_saas_site_id.label\": \"阿里云 ESA 站点 ID\",\r\n  \"workflow_node.deploy.form.aliyun_esa_saas_site_id.placeholder\": \"请输入阿里云 ESA 站点 ID\",\r\n  \"workflow_node.deploy.form.aliyun_esa_saas_site_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://esa.console.aliyun.com/siteManage/list\\\" target=\\\"_blank\\\">https://esa.console.aliyun.com/siteManage/list</a>\",\r\n  \"workflow_node.deploy.form.aliyun_esa_saas_domain.label\": \"阿里云 ESA SaaS 域名\",\r\n  \"workflow_node.deploy.form.aliyun_esa_saas_domain.placeholder\": \"请输入阿里云 ESA SaaS 域名\",\r\n  \"workflow_node.deploy.form.aliyun_fc_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.deploy.form.aliyun_fc_region.placeholder\": \"请输入阿里云 FC 服务地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.deploy.form.aliyun_fc_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/functioncompute/fc-3-0/product-overview/supported-regions\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/functioncompute/fc-3-0/product-overview/supported-regions</a>\",\r\n  \"workflow_node.deploy.form.aliyun_fc_service_version.label\": \"阿里云 FC 服务版本\",\r\n  \"workflow_node.deploy.form.aliyun_fc_service_version.placeholder\": \"请选择阿里云 FC 服务版本\",\r\n  \"workflow_node.deploy.form.aliyun_fc_domain.label\": \"阿里云 FC 自定义域名\",\r\n  \"workflow_node.deploy.form.aliyun_fc_domain.placeholder\": \"请输入阿里云 FC 自定义域名\",\r\n  \"workflow_node.deploy.form.aliyun_ga_resource_type.option.accelerator.label\": \"部署到指定全球加速器下的全部 HTTPS 监听\",\r\n  \"workflow_node.deploy.form.aliyun_ga_resource_type.option.listener.label\": \"部署到指定 HTTPS 监听器\",\r\n  \"workflow_node.deploy.form.aliyun_ga_accelerator_id.label\": \"阿里云全球加速实例 ID\",\r\n  \"workflow_node.deploy.form.aliyun_ga_accelerator_id.placeholder\": \"请输入阿里云全球加速实例 ID\",\r\n  \"workflow_node.deploy.form.aliyun_ga_accelerator_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://ga.console.aliyun.com\\\" target=\\\"_blank\\\">https://ga.console.aliyun.com</a>\",\r\n  \"workflow_node.deploy.form.aliyun_ga_listener_id.label\": \"阿里云全球加速监听 ID\",\r\n  \"workflow_node.deploy.form.aliyun_ga_listener_id.placeholder\": \"请输入阿里云全球加速监听 ID\",\r\n  \"workflow_node.deploy.form.aliyun_ga_listener_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://ga.console.aliyun.com\\\" target=\\\"_blank\\\">https://ga.console.aliyun.com</a>\",\r\n  \"workflow_node.deploy.form.aliyun_ga_snidomain.label\": \"阿里云全球加速扩展域名（可选）\",\r\n  \"workflow_node.deploy.form.aliyun_ga_snidomain.placeholder\": \"请输入阿里云全球加速扩展域名\",\r\n  \"workflow_node.deploy.form.aliyun_ga_snidomain.help\": \"提示：不填写时，将替换监听器的默认证书；否则，将替换扩展域名证书。\",\r\n  \"workflow_node.deploy.form.aliyun_live_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.deploy.form.aliyun_live_region.placeholder\": \"请输入阿里云视频直播服务地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.deploy.form.aliyun_live_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/live/product-overview/supported-regions\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/live/product-overview/supported-regions</a>\",\r\n  \"workflow_node.deploy.form.aliyun_live_domain.label\": \"阿里云视频直播流域名\",\r\n  \"workflow_node.deploy.form.aliyun_live_domain.placeholder\": \"请输入阿里云视频直播流域名\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_region.placeholder\": \"请输入阿里云 NLB 服务地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/slb/network-load-balancer/product-overview/regions-that-support-nlb\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/slb/network-load-balancer/product-overview/regions-that-support-nlb</a>\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_resource_type.option.loadbalancer.label\": \"部署到指定负载均衡器下的全部 HTTPS/QUIC 监听器\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_resource_type.option.listener.label\": \"部署到指定 HTTPS/QUIC 监听器\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_loadbalancer_id.label\": \"阿里云 NLB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_loadbalancer_id.placeholder\": \"请输入阿里云 NLB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_loadbalancer_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://slb.console.aliyun.com/nlb\\\" target=\\\"_blank\\\">https://slb.console.aliyun.com/nlb</a>\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_listener_id.label\": \"阿里云 NLB 监听器 ID\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_listener_id.placeholder\": \"请输入阿里云 NLB 监听器 ID\",\r\n  \"workflow_node.deploy.form.aliyun_nlb_listener_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://slb.console.aliyun.com/nlb\\\" target=\\\"_blank\\\">https://slb.console.aliyun.com/nlb</a>\",\r\n  \"workflow_node.deploy.form.aliyun_oss_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.deploy.form.aliyun_oss_region.placeholder\": \"请输入阿里云 OSS 服务地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.deploy.form.aliyun_oss_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/oss/user-guide/regions-and-endpoints\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/oss/user-guide/regions-and-endpoints</a>\",\r\n  \"workflow_node.deploy.form.aliyun_oss_bucket.label\": \"阿里云 OSS 存储桶名\",\r\n  \"workflow_node.deploy.form.aliyun_oss_bucket.placeholder\": \"请输入阿里云 OSS 存储桶名\",\r\n  \"workflow_node.deploy.form.aliyun_oss_domain.label\": \"阿里云 OSS 自定义域名\",\r\n  \"workflow_node.deploy.form.aliyun_oss_domain.placeholder\": \"请输入阿里云 OSS 自定义域名\",\r\n  \"workflow_node.deploy.form.aliyun_vod_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.deploy.form.aliyun_vod_region.placeholder\": \"请输入阿里云视频点播服务地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.deploy.form.aliyun_vod_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/vod/product-overview/regions\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/vod/product-overview/regions</a>\",\r\n  \"workflow_node.deploy.form.aliyun_vod_domain.label\": \"阿里云视频点播加速域名\",\r\n  \"workflow_node.deploy.form.aliyun_vod_domain.placeholder\": \"请输入阿里云视频点播加速域名\",\r\n  \"workflow_node.deploy.form.aliyun_waf_region.label\": \"阿里云服务地域\",\r\n  \"workflow_node.deploy.form.aliyun_waf_region.placeholder\": \"请输入阿里云 WAF 服务地域（例如：cn-hangzhou）\",\r\n  \"workflow_node.deploy.form.aliyun_waf_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-endpoint\\\" target=\\\"_blank\\\">https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-endpoint</a>\",\r\n  \"workflow_node.deploy.form.aliyun_waf_service_version.label\": \"阿里云 WAF 服务版本\",\r\n  \"workflow_node.deploy.form.aliyun_waf_service_version.placeholder\": \"请选择阿里云 WAF 服务版本\",\r\n  \"workflow_node.deploy.form.aliyun_waf_service_type.label\": \"阿里云 WAF 服务接入方式\",\r\n  \"workflow_node.deploy.form.aliyun_waf_service_type.placeholder\": \"请选择阿里云 WAF 服务接入方式\",\r\n  \"workflow_node.deploy.form.aliyun_waf_service_type.option.cloudresource.label\": \"云产品接入\",\r\n  \"workflow_node.deploy.form.aliyun_waf_service_type.option.cname.label\": \"CNAME 接入\",\r\n  \"workflow_node.deploy.form.aliyun_waf_instance_id.label\": \"阿里云 WAF 实例 ID\",\r\n  \"workflow_node.deploy.form.aliyun_waf_instance_id.placeholder\": \"请输入阿里云 WAF 实例 ID\",\r\n  \"workflow_node.deploy.form.aliyun_waf_instance_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://waf.console.aliyun.com\\\" target=\\\"_blank\\\">https://waf.console.aliyun.com</a>\",\r\n  \"workflow_node.deploy.form.aliyun_waf_resource_product.label\": \"阿里云 WAF 云产品接入资源类型\",\r\n  \"workflow_node.deploy.form.aliyun_waf_resource_product.placeholder\": \"请选择 WAF 云产品接入资源类型\",\r\n  \"workflow_node.deploy.form.aliyun_waf_resource_id.label\": \"阿里云 WAF 云产品接入资源 ID\",\r\n  \"workflow_node.deploy.form.aliyun_waf_resource_id.placeholder\": \"请选择阿里云 WAF 云产品接入资源 ID\",\r\n  \"workflow_node.deploy.form.aliyun_waf_resource_port.label\": \"阿里云 WAF 云产品接入端口\",\r\n  \"workflow_node.deploy.form.aliyun_waf_resource_port.placeholder\": \"请选择阿里云 WAF 云产品接入端口\",\r\n  \"workflow_node.deploy.form.aliyun_waf_domain.label\": \"阿里云 WAF 扩展域名（可选）\",\r\n  \"workflow_node.deploy.form.aliyun_waf_domain.placeholder\": \"请输入阿里云 WAF 扩展域名\",\r\n  \"workflow_node.deploy.form.aliyun_waf_domain.help\": \"提示：不填写时，将替换实例的默认证书；否则，将替换扩展域名证书。\",\r\n  \"workflow_node.deploy.form.apisix.guide\": \"需要 APISIX v2.0 或更高版本。\",\r\n  \"workflow_node.deploy.form.apisix_resource_type.option.certificate.label\": \"替换指定证书\",\r\n  \"workflow_node.deploy.form.apisix_certificate_id.label\": \"APISIX 证书 ID\",\r\n  \"workflow_node.deploy.form.apisix_certificate_id.placeholder\": \"请输入 APISIX 证书 ID\",\r\n  \"workflow_node.deploy.form.apisix_certificate_id.tooltip\": \"请登录 APISIX 控制台查看\",\r\n  \"workflow_node.deploy.form.aws_acm_region.label\": \"AWS 服务区域\",\r\n  \"workflow_node.deploy.form.aws_acm_region.placeholder\": \"请输入 AWS ACM 服务区域（例如：us-east-1）\",\r\n  \"workflow_node.deploy.form.aws_acm_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints</a>\",\r\n  \"workflow_node.deploy.form.aws_acm_certificate_arn.label\": \"AWS ACM 证书 ARN（可选）\",\r\n  \"workflow_node.deploy.form.aws_acm_certificate_arn.placeholder\": \"请输入 AWS ACM 证书 ARN\",\r\n  \"workflow_node.deploy.form.aws_acm_certificate_arn.help\": \"提示：不填写时，将导入为新证书；否则，将替换原证书。\",\r\n  \"workflow_node.deploy.form.aws_cloudfront_region.label\": \"AWS 服务区域\",\r\n  \"workflow_node.deploy.form.aws_cloudfront_region.placeholder\": \"请输入 AWS CloudFront 服务区域（例如：us-east-1）\",\r\n  \"workflow_node.deploy.form.aws_cloudfront_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints</a>\",\r\n  \"workflow_node.deploy.form.aws_cloudfront_distribution_id.label\": \"AWS CloudFront 分配 ID\",\r\n  \"workflow_node.deploy.form.aws_cloudfront_distribution_id.placeholder\": \"请输入 AWS CloudFront 分配 ID\",\r\n  \"workflow_node.deploy.form.aws_cloudfront_distribution_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.aws.amazon.com/zh_cn/AmazonCloudFront/latest/DeveloperGuide/distribution-working-with.html\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/zh_cn/AmazonCloudFront/latest/DeveloperGuide/distribution-working-with.html</a>\",\r\n  \"workflow_node.deploy.form.aws_cloudfront_certificate_source.label\": \"AWS CloudFront 证书来源\",\r\n  \"workflow_node.deploy.form.aws_cloudfront_certificate_source.placeholder\": \"请选择 AWS CloudFront 证书来源\",\r\n  \"workflow_node.deploy.form.aws_iam_region.label\": \"AWS 服务区域\",\r\n  \"workflow_node.deploy.form.aws_iam_region.placeholder\": \"请输入 AWS IAM 服务区域（例如：us-east-1）\",\r\n  \"workflow_node.deploy.form.aws_iam_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints</a>\",\r\n  \"workflow_node.deploy.form.aws_iam_certificate_path.label\": \"AWS IAM 证书路径（可选）\",\r\n  \"workflow_node.deploy.form.aws_iam_certificate_path.placeholder\": \"请输入 AWS IAM 证书路径\",\r\n  \"workflow_node.deploy.form.aws_iam_certificate_path.errmsg.invalid\": \"请输入正确的 AWS IAM 证书路径\",\r\n  \"workflow_node.deploy.form.aws_iam_certificate_path.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/reference_identifiers.html\\\" target=\\\"_blank\\\">https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/reference_identifiers.html</a>\",\r\n  \"workflow_node.deploy.form.azure_keyvault_name.label\": \"Azure KeyVault 名称\",\r\n  \"workflow_node.deploy.form.azure_keyvault_name.placeholder\": \"请输入 Azure KeyVault 名称\",\r\n  \"workflow_node.deploy.form.azure_keyvault_name.tooltip\": \"这是什么？请参阅 <a href=\\\"https://learn.microsoft.com/zh-cn/azure/key-vault/general/about-keys-secrets-certificates\\\" target=\\\"_blank\\\">https://learn.microsoft.com/zh-cn/azure/key-vault/general/about-keys-secrets-certificates</a>\",\r\n  \"workflow_node.deploy.form.azure_keyvault_certificate_name.label\": \"Azure KeyVault 证书名称（可选）\",\r\n  \"workflow_node.deploy.form.azure_keyvault_certificate_name.placeholder\": \"请输入 Azure KeyVault 证书名称\",\r\n  \"workflow_node.deploy.form.azure_keyvault_certificate_name.errmsg.invalid\": \"证书名称只能包含字母、数字和连字符（-），长度限制为 1 到 127 个字符\",\r\n  \"workflow_node.deploy.form.azure_keyvault_certificate_name.help\": \"提示：不填写时，将由 Certimate 自动生成证书名称。\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_region.label\": \"百度智能云服务地域\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_region.placeholder\": \"请输入百度智能云 BLB 服务地域（例如：bj）\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.baidu.com/doc/BLB/s/cjwvxnzix\\\" target=\\\"_blank\\\">https://cloud.baidu.com/doc/BLB/s/cjwvxnzix</a>\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_resource_type.option.loadbalancer.label\": \"部署到指定负载均衡器下的全部 HTTPS/SSL 监听\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_resource_type.option.listener.label\": \"部署到指定 HTTPS/SSL 监听\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.label\": \"百度智能云 BLB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder\": \"请输入百度智能云 BLB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.bce.baidu.com/blb/#/appblb/list\\\" target=\\\"_blank\\\">https://console.bce.baidu.com/blb/#/appblb/list</a>\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_listener_port.label\": \"百度智能云 BLB 监听端口\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_listener_port.placeholder\": \"请输入百度智能云 BLB 监听端口\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_listener_port.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.bce.baidu.com/blb/#/appblb/list\\\" target=\\\"_blank\\\">https://console.bce.baidu.com/blb/#/appblb/list</a>\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_snidomain.label\": \"百度智能云 BLB 扩展域名（可选）\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_snidomain.placeholder\": \"请输入百度智能云 BLB 扩展域名\",\r\n  \"workflow_node.deploy.form.baiducloud_appblb_snidomain.help\": \"提示：不填写时，将替换监听器的默认证书；否则，将替换扩展域名证书。\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_region.label\": \"百度智能云服务地域\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_region.placeholder\": \"请输入百度智能云 BLB 服务地域（例如：bj）\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.baidu.com/doc/BLB/s/cjwvxnzix\\\" target=\\\"_blank\\\">https://cloud.baidu.com/doc/BLB/s/cjwvxnzix</a>\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_resource_type.option.loadbalancer.label\": \"部署到指定负载均衡器下的全部 HTTPS/SSL 监听\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_resource_type.option.listener.label\": \"部署到指定 HTTPS/SSL 监听\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.label\": \"百度智能云 BLB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder\": \"请输入百度智能云 BLB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.bce.baidu.com/blb/#/blb/list\\\" target=\\\"_blank\\\">https://console.bce.baidu.com/blb/#/blb/list</a>\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_listener_port.label\": \"百度智能云 BLB 监听端口\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_listener_port.placeholder\": \"请输入百度智能云 BLB 监听端口\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_listener_port.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.bce.baidu.com/blb/#/blb/list\\\" target=\\\"_blank\\\">https://console.bce.baidu.com/blb/#/blb/list</a>\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_snidomain.label\": \"百度智能云 BLB 扩展域名（可选）\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_snidomain.placeholder\": \"请输入百度智能云 BLB 扩展域名\",\r\n  \"workflow_node.deploy.form.baiducloud_blb_snidomain.help\": \"提示：不填写时，将替换监听器的默认证书；否则，将替换扩展域名证书。\",\r\n  \"workflow_node.deploy.form.baiducloud_cdn_domain.label\": \"百度智能云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.baiducloud_cdn_domain.placeholder\": \"请输入百度智能云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.baishan_cdn_resource_type.option.domain.label\": \"部署到指定加速域名\",\r\n  \"workflow_node.deploy.form.baishan_cdn_resource_type.option.certificate.label\": \"替换指定证书\",\r\n  \"workflow_node.deploy.form.baishan_cdn_domain.label\": \"白山云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.baishan_cdn_domain.placeholder\": \"请输入白山云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.baishan_cdn_certificate_id.label\": \"白山云 CDN 证书 ID\",\r\n  \"workflow_node.deploy.form.baishan_cdn_certificate_id.placeholder\": \"请输入白山云 CDN 证书 ID\",\r\n  \"workflow_node.deploy.form.baishan_cdn_certificate_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cdnx.console.baishan.com/#/cdn/cert\\\" target=\\\"_blank\\\">https://cdnx.console.baishan.com/#/cdn/cert</a>\",\r\n  \"workflow_node.deploy.form.baotapanel.guide\": \"需要宝塔面板 v8.3 或更高版本。\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.label\": \"宝塔面板网站类型\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.placeholder\": \"请选择宝塔面板网站类型\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.php.label\": \"PHP 项目\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.java.label\": \"Java 项目\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.nodejs.label\": \"Node.js 项目\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.go.label\": \"Golang 项目\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.python.label\": \"Python 项目\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.proxy.label\": \"反向代理\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.html.label\": \"HTML 项目\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.general.label\": \"通用项目\",\r\n  \"workflow_node.deploy.form.baotapanel_site_type.option.any.label\": \"任意类型（需要宝塔面板 v9.4+）\",\r\n  \"workflow_node.deploy.form.baotapanel_site_names.label\": \"宝塔面板网站名称\",\r\n  \"workflow_node.deploy.form.baotapanel_site_names.placeholder\": \"请输入宝塔面板网站名称（多个值请用半角分号隔开）\",\r\n  \"workflow_node.deploy.form.baotapanel_site_names.errmsg.invalid\": \"请输入正确的宝塔面板网站名称\",\r\n  \"workflow_node.deploy.form.baotapanel_site_names.help\": \"提示：支持多个网站名称，以半角分号隔开。\",\r\n  \"workflow_node.deploy.form.baotapanel_site_names.tooltip\": \"请登录宝塔面板查看\",\r\n  \"workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.title\": \"修改宝塔面板网站名称\",\r\n  \"workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.placeholder\": \"请输入宝塔面板网站名称\",\r\n  \"workflow_node.deploy.form.baotapanel_console.guide\": \"需要宝塔面板 v8.3 或更高版本。\",\r\n  \"workflow_node.deploy.form.baotapanel_console_auto_restart.label\": \"部署后自动重启宝塔面板服务\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.label\": \"宝塔面板极速版网站类型\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.placeholder\": \"请选择宝塔面板极速版网站类型\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.option.php.label\": \"PHP 项目\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.option.java.label\": \"Java 项目\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.option.asp.label\": \".NET 项目\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.option.go.label\": \"Golang 项目\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.option.python.label\": \"Python 项目\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.option.nodejs.label\": \"Node.js 项目\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.option.proxy.label\": \"反向代理\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_type.option.general.label\": \"通用项目\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_names.label\": \"宝塔面板极速版网站名称\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_names.placeholder\": \"请输入宝塔面板极速版网站名称（多个值请用半角分号隔开）\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_names.errmsg.invalid\": \"请输入正确的宝塔面板极速版网站名称\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_names.help\": \"提示：支持多个网站名称，以半角分号隔开。\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_names.tooltip\": \"请登录宝塔面板极速版查看\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_names.multiple_input_modal.title\": \"修改宝塔面板极速版网站名称\",\r\n  \"workflow_node.deploy.form.baotapanelgo_site_names.multiple_input_modal.placeholder\": \"请输入宝塔面板极速版网站名称\",\r\n  \"workflow_node.deploy.form.baotawaf.guide\": \"需要堡塔云 WAF v5.7 或更高版本。\",\r\n  \"workflow_node.deploy.form.baotawaf_site_names.label\": \"堡塔云 WAF 网站名称\",\r\n  \"workflow_node.deploy.form.baotawaf_site_names.placeholder\": \"请输入堡塔云 WAF 网站名称（多个值请用半角分号隔开）\",\r\n  \"workflow_node.deploy.form.baotawaf_site_names.errmsg.invalid\": \"请输入正确的堡塔云 WAF 网站名称\",\r\n  \"workflow_node.deploy.form.baotawaf_site_names.help\": \"提示：支持多个网站名称，以半角分号隔开。\",\r\n  \"workflow_node.deploy.form.baotawaf_site_names.tooltip\": \"请登录堡塔云 WAF 查看\",\r\n  \"workflow_node.deploy.form.baotawaf_site_names.multiple_input_modal.title\": \"修改堡塔云 WAF 网站名称\",\r\n  \"workflow_node.deploy.form.baotawaf_site_names.multiple_input_modal.placeholder\": \"请输入堡塔云 WAF 网站名称\",\r\n  \"workflow_node.deploy.form.baotawaf_site_port.label\": \"堡塔云 WAF 网站 SSL 端口\",\r\n  \"workflow_node.deploy.form.baotawaf_site_port.placeholder\": \"请输入堡塔云 WAF 网站 SSL 端口\",\r\n  \"workflow_node.deploy.form.baotawaf_console.guide\": \"需要堡塔云 WAF v5.7 或更高版本。\",\r\n  \"workflow_node.deploy.form.bunny_cdn_pull_zone_id.label\": \"Bunny CDN 拉取区域 ID\",\r\n  \"workflow_node.deploy.form.bunny_cdn_pull_zone_id.placeholder\": \"请输入 Bunny CDN 拉取区域 ID\",\r\n  \"workflow_node.deploy.form.bunny_cdn_pull_zone_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://dash.bunny.net/cdn\\\" target=\\\"_blank\\\">https://dash.bunny.net/cdn</a>\",\r\n  \"workflow_node.deploy.form.bunny_cdn_hostname.label\": \"Bunny CDN 主机名\",\r\n  \"workflow_node.deploy.form.bunny_cdn_hostname.placeholder\": \"请输入 Bunny CDN 主机名\",\r\n  \"workflow_node.deploy.form.bunny_cdn_hostname.tooltip\": \"这是什么？请参阅 <a href=\\\"https://dash.bunny.net/cdn\\\" target=\\\"_blank\\\">https://dash.bunny.net/cdn</a>\",\r\n  \"workflow_node.deploy.form.byteplus_cdn_domain.label\": \"BytePlus CDN 域名\",\r\n  \"workflow_node.deploy.form.byteplus_cdn_domain.placeholder\": \"请输入 BytePlus CDN 域名\",\r\n  \"workflow_node.deploy.form.cdnfly_resource_type.option.website.label\": \"部署到指定网站\",\r\n  \"workflow_node.deploy.form.cdnfly_resource_type.option.certificate.label\": \"替换指定证书\",\r\n  \"workflow_node.deploy.form.cdnfly_site_id.label\": \"Cdnfly 网站 ID\",\r\n  \"workflow_node.deploy.form.cdnfly_site_id.placeholder\": \"请输入 Cdnfly 网站 ID\",\r\n  \"workflow_node.deploy.form.cdnfly_site_id.tooltip\": \"请登录 Cdnfly 控制台查看\",\r\n  \"workflow_node.deploy.form.cdnfly_certificate_id.label\": \"Cdnfly 证书 ID\",\r\n  \"workflow_node.deploy.form.cdnfly_certificate_id.placeholder\": \"请输入 Cdnfly 证书 ID\",\r\n  \"workflow_node.deploy.form.cdnfly_certificate_id.tooltip\": \"请登录 Cdnfly 控制台查看\",\r\n  \"workflow_node.deploy.form.cpanel_resource_type.option.website.label\": \"部署到指定网站\",\r\n  \"workflow_node.deploy.form.cpanel_domain.label\": \"cPanel 网站域名\",\r\n  \"workflow_node.deploy.form.cpanel_domain.placeholder\": \"请输入 cPanel 网站域名\",\r\n  \"workflow_node.deploy.form.ctcccloud_ao_domain.label\": \"天翼云 AccessOne 加速域名\",\r\n  \"workflow_node.deploy.form.ctcccloud_ao_domain.placeholder\": \"请输入天翼云 AccessOne 加速域名\",\r\n  \"workflow_node.deploy.form.ctcccloud_cdn_domain.label\": \"天翼云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.ctcccloud_cdn_domain.placeholder\": \"请输入天翼云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_region_id.label\": \"天翼云资源池 ID\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_region_id.placeholder\": \"请输入天翼云 ELB 资源池 ID\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_region_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.ctyun.cn/document/10026755/10196575\\\" target=\\\"_blank\\\">https://www.ctyun.cn/document/10026755/10196575</a>\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_resource_type.option.loadbalancer.label\": \"部署到指定负载均衡器下的全部 HTTPS 监听器\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_resource_type.option.listener.label\": \"部署到指定 HTTPS 监听器\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.label\": \"天翼云 ELB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.placeholder\": \"请输入天翼云 ELB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.ctyun.cn/network/index/#/elb/elbList\\\" target=\\\"_blank\\\">https://console.ctyun.cn/network/index/#/elb/elbList</a>\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_listener_id.label\": \"天翼云 ELB 监听器 ID\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_listener_id.placeholder\": \"请输入天翼云 ELB 监听器 ID\",\r\n  \"workflow_node.deploy.form.ctcccloud_elb_listener_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.ctyun.cn/network/index/#/elb/elbList\\\" target=\\\"_blank\\\">https://console.ctyun.cn/network/index/#/elb/elbList</a>\",\r\n  \"workflow_node.deploy.form.ctcccloud_faas_region_id.label\": \"天翼云资源池 ID\",\r\n  \"workflow_node.deploy.form.ctcccloud_faas_region_id.placeholder\": \"请输入天翼云 FaaS 资源池 ID\",\r\n  \"workflow_node.deploy.form.ctcccloud_faas_region_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.ctyun.cn/document/10026755/10196575\\\" target=\\\"_blank\\\">https://www.ctyun.cn/document/10026755/10196575</a>\",\r\n  \"workflow_node.deploy.form.ctcccloud_faas_domain.label\": \"天翼云 FaaS 自定义域名\",\r\n  \"workflow_node.deploy.form.ctcccloud_faas_domain.placeholder\": \"请输入天翼云 FaaS 自定义域名\",\r\n  \"workflow_node.deploy.form.ctcccloud_icdn_domain.label\": \"天翼云 ICDN 加速域名\",\r\n  \"workflow_node.deploy.form.ctcccloud_icdn_domain.placeholder\": \"请输入天翼云 ICDN 加速域名\",\r\n  \"workflow_node.deploy.form.ctcccloud_lvdn_domain.label\": \"天翼云 LVDN 加速域名\",\r\n  \"workflow_node.deploy.form.ctcccloud_lvdn_domain.placeholder\": \"请输入天翼云 LVDN 加速域名\",\r\n  \"workflow_node.deploy.form.dogecloud_cdn_domain.label\": \"多吉云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.dogecloud_cdn_domain.placeholder\": \"请输入多吉云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.flexcdn_resource_type.option.certificate.label\": \"替换指定证书\",\r\n  \"workflow_node.deploy.form.flexcdn_certificate_id.label\": \"FlexCDN 证书 ID\",\r\n  \"workflow_node.deploy.form.flexcdn_certificate_id.placeholder\": \"请输入 FlexCDN 证书 ID\",\r\n  \"workflow_node.deploy.form.flexcdn_certificate_id.tooltip\": \"请登录 FlexCDN 控制台查看\",\r\n  \"workflow_node.deploy.form.flyio_app_name.label\": \"Fly.io 应用名称\",\r\n  \"workflow_node.deploy.form.flyio_app_name.placeholder\": \"请输入 Fly.io 应用名称\",\r\n  \"workflow_node.deploy.form.flyio_domain.label\": \"Fly.io 自定义域名\",\r\n  \"workflow_node.deploy.form.flyio_domain.placeholder\": \"请输入 Fly.io 自定义域名\",\r\n  \"workflow_node.deploy.form.flyio_domain.help\": \"注意：首次导入自定义证书后，请登录 Fly.io 控制台完成域名所有权验证。\",\r\n  \"workflow_node.deploy.form.gcore_cdn_resource_id.label\": \"G-Core CDN 资源 ID\",\r\n  \"workflow_node.deploy.form.gcore_cdn_resource_id.placeholder\": \"请输入 G-Core CDN 资源 ID\",\r\n  \"workflow_node.deploy.form.gcore_cdn_resource_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cdn.gcore.com/resources/list\\\" target=\\\"_blank\\\">https://cdn.gcore.com/resources/list</a>\",\r\n  \"workflow_node.deploy.form.gcore_cdn_certificate_id.label\": \"G-Core CDN 原证书 ID（可选）\",\r\n  \"workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder\": \"请输入 G-Core CDN 原证书 ID\",\r\n  \"workflow_node.deploy.form.gcore_cdn_certificate_id.help\": \"提示：不填写时，将上传新证书；否则，将替换原证书。\",\r\n  \"workflow_node.deploy.form.gcore_cdn_certificate_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cdn.gcore.com/ssl\\\" target=\\\"_blank\\\">https://cdn.gcore.com/ssl</a>\",\r\n  \"workflow_node.deploy.form.goedge_resource_type.option.certificate.label\": \"替换指定证书\",\r\n  \"workflow_node.deploy.form.goedge_certificate_id.label\": \"GoEdge 证书 ID\",\r\n  \"workflow_node.deploy.form.goedge_certificate_id.placeholder\": \"请输入 GoEdge 证书 ID\",\r\n  \"workflow_node.deploy.form.goedge_certificate_id.tooltip\": \"请登录 GoEdge 控制台查看\",\r\n  \"workflow_node.deploy.form.huaweicloud_cdn_region.label\": \"华为云服务区域\",\r\n  \"workflow_node.deploy.form.huaweicloud_cdn_region.placeholder\": \"请输入华为云 CDN 服务区域（例如：cn-north-1）\",\r\n  \"workflow_node.deploy.form.huaweicloud_cdn_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.huaweicloud.com/apiexplorer/#/endpoint\\\" target=\\\"_blank\\\">https://console.huaweicloud.com/apiexplorer/#/endpoint</a>\",\r\n  \"workflow_node.deploy.form.huaweicloud_cdn_domain.label\": \"华为云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.huaweicloud_cdn_domain.placeholder\": \"请输入华为云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.huaweicloud_obs_region.label\": \"华为云服务区域\",\r\n  \"workflow_node.deploy.form.huaweicloud_obs_region.placeholder\": \"请输入华为云 OBS 服务区域（例如：cn-north-1）\",\r\n  \"workflow_node.deploy.form.huaweicloud_obs_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.huaweicloud.com/apiexplorer/#/endpoint\\\" target=\\\"_blank\\\">https://console.huaweicloud.com/apiexplorer/#/endpoint</a>\",\r\n  \"workflow_node.deploy.form.huaweicloud_obs_bucket.label\": \"华为云 OBS 存储桶名\",\r\n  \"workflow_node.deploy.form.huaweicloud_obs_bucket.placeholder\": \"请输入华为云 OBS 存储桶名\",\r\n  \"workflow_node.deploy.form.huaweicloud_obs_domain.label\": \"华为云 OBS 自定义域名\",\r\n  \"workflow_node.deploy.form.huaweicloud_obs_domain.placeholder\": \"请输入华为云 OBS 自定义域名\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_region.label\": \"华为云服务区域\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_region.placeholder\": \"请输入华为云 ELB 服务区域（例如：cn-north-1）\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.huaweicloud.com/apiexplorer/#/endpoint\\\" target=\\\"_blank\\\">https://console.huaweicloud.com/apiexplorer/#/endpoint</a>\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_resource_type.option.loadbalancer.label\": \"部署到指定负载均衡器下的全部 HTTPS 监听器\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_resource_type.option.listener.label\": \"部署到指定 HTTPS 监听器\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_resource_type.option.certificate.label\": \"替换指定证书\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_certificate_id.label\": \"华为云 ELB 证书 ID\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_certificate_id.placeholder\": \"请输入华为云 ELB 证书 ID\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_certificate_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.huaweicloud.com/vpc/#/elb/elbCert\\\" target=\\\"_blank\\\">https://console.huaweicloud.com/vpc/#/elb/elbCert</a>\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_loadbalancer_id.label\": \"华为云 ELB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_loadbalancer_id.placeholder\": \"请输入华为云 ELB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_loadbalancer_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.huaweicloud.com/vpc/#/elb/list/grid\\\" target=\\\"_blank\\\">https://console.huaweicloud.com/vpc/#/elb/list/grid</a>\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_listener_id.label\": \"华为云 ELB 监听器 ID\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_listener_id.placeholder\": \"请输入华为云 ELB 监听器 ID\",\r\n  \"workflow_node.deploy.form.huaweicloud_elb_listener_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.huaweicloud.com/vpc/#/elb/list/grid\\\" target=\\\"_blank\\\">https://console.huaweicloud.com/vpc/#/elb/list/grid</a>\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_region.label\": \"华为云服务区域\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_region.placeholder\": \"请输入华为云 WAF 服务区域（例如：cn-north-1）\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.huaweicloud.com/apiexplorer/#/endpoint\\\" target=\\\"_blank\\\">https://console.huaweicloud.com/apiexplorer/#/endpoint</a>\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_resource_type.option.cloudserver.label\": \"部署到指定云模式防护网站\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_resource_type.option.premiumhost.label\": \"部署到指定独享模式防护网站\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_resource_type.option.certificate.label\": \"替换指定证书\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_domain.label\": \"华为云 WAF 防护域名\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_domain.placeholder\": \"请输入华为云 WAF 防护域名\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_certificate_id.label\": \"华为云 WAF 证书 ID\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_certificate_id.placeholder\": \"请输入华为云 WAF 证书 ID\",\r\n  \"workflow_node.deploy.form.huaweicloud_waf_certificate_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.huaweicloud.com/console/#/waf/certificateManagement\\\" target=\\\"_blank\\\">https://console.huaweicloud.com/console/#/waf/certificateManagement</a>\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_region_id.label\": \"京东云服务地域 ID\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_region_id.placeholder\": \"请输入京东云 ALB 服务地域 ID（例如：cn-north-1\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_region_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.jdcloud.com/cn/common-declaration/api/introduction\\\" target=\\\"_blank\\\">https://docs.jdcloud.com/cn/common-declaration/api/introduction</a>\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_resource_type.option.loadbalancer.label\": \"部署到指定负载均衡器下的全部 HTTPS/TLS 监听\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_resource_type.option.listener.label\": \"部署到指定 HTTPS/TLS 监听器\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_loadbalancer_id.label\": \"京东云 ALB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_loadbalancer_id.placeholder\": \"请输入京东云 ALB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_loadbalancer_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cns-console.jdcloud.com/host/loadBalance/list\\\" target=\\\"_blank\\\">https://cns-console.jdcloud.com/host/loadBalance/list</a>\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_listener_id.label\": \"京东云 ALB 监听器 ID\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_listener_id.placeholder\": \"请输入京东云 ALB 监听器 ID\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_listener_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cns-console.jdcloud.com/host/loadBalance/list\\\" target=\\\"_blank\\\">https://cns-console.jdcloud.com/host/loadBalance/list</a>\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_snidomain.label\": \"京东云 ALB 扩展域名（可选）\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_snidomain.placeholder\": \"请输入京东云 ALB 扩展域名\",\r\n  \"workflow_node.deploy.form.jdcloud_alb_snidomain.help\": \"提示：不填写时，将替换监听器的默认证书；否则，将替换扩展域名证书。\",\r\n  \"workflow_node.deploy.form.jdcloud_cdn_domain.label\": \"京东云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.jdcloud_cdn_domain.placeholder\": \"请输入京东云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.jdcloud_live_domain.label\": \"京东云视频直播播放域名\",\r\n  \"workflow_node.deploy.form.jdcloud_live_domain.placeholder\": \"请输入京东云视频直播播放域名\",\r\n  \"workflow_node.deploy.form.jdcloud_vod_domain.label\": \"京东云视频点播加速域名\",\r\n  \"workflow_node.deploy.form.jdcloud_vod_domain.placeholder\": \"请输入京东云视频点播加速域名\",\r\n  \"workflow_node.deploy.form.k8s_namespace.label\": \"Kubernetes 命名空间\",\r\n  \"workflow_node.deploy.form.k8s_namespace.placeholder\": \"请输入 Kubernetes 命名空间\",\r\n  \"workflow_node.deploy.form.k8s_namespace.tooltip\": \"这是什么？请参阅 <a href=\\\"https://kubernetes.io/zh-cn/docs/concepts/overview/working-with-objects/namespaces/\\\" target=\\\"_blank\\\">https://kubernetes.io/zh-cn/docs/concepts/overview/working-with-objects/namespaces/</a>\",\r\n  \"workflow_node.deploy.form.k8s_secret_name.label\": \"Kubernetes Secret 名称\",\r\n  \"workflow_node.deploy.form.k8s_secret_name.placeholder\": \"请输入 Kubernetes Secret 名称\",\r\n  \"workflow_node.deploy.form.k8s_secret_name.tooltip\": \"这是什么？请参阅 <a href=\\\"https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/\\\" target=\\\"_blank\\\">https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/</a>\",\r\n  \"workflow_node.deploy.form.k8s_secret_type.label\": \"Kubernetes Secret 类型\",\r\n  \"workflow_node.deploy.form.k8s_secret_type.placeholder\": \"请输入 Kubernetes Secret 类型\",\r\n  \"workflow_node.deploy.form.k8s_secret_type.tooltip\": \"这是什么？请参阅 <a href=\\\"https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/\\\" target=\\\"_blank\\\">https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/</a>\",\r\n  \"workflow_node.deploy.form.k8s_secret_data_key_for_crt.label\": \"Kubernetes Secret 数据键（用于存放证书的字段）\",\r\n  \"workflow_node.deploy.form.k8s_secret_data_key_for_crt.placeholder\": \"请输入 Kubernetes Secret 中用于存放证书的数据键\",\r\n  \"workflow_node.deploy.form.k8s_secret_data_key_for_crt.tooltip\": \"这是什么？请参阅 <a href=\\\"https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/\\\" target=\\\"_blank\\\">https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/</a>\",\r\n  \"workflow_node.deploy.form.k8s_secret_data_key_for_key.label\": \"Kubernetes Secret 数据键（用于存放私钥的字段）\",\r\n  \"workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder\": \"请输入 Kubernetes Secret 中用于存放私钥的数据键\",\r\n  \"workflow_node.deploy.form.k8s_secret_data_key_for_key.tooltip\": \"这是什么？请参阅 <a href=\\\"https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/\\\" target=\\\"_blank\\\">https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/</a>\",\r\n  \"workflow_node.deploy.form.k8s_secret_annotations.label\": \"Kubernetes Secret 注解（可选）\",\r\n  \"workflow_node.deploy.form.k8s_secret_annotations.placeholder\": \"请输入 Kubernetes Secret 注解\",\r\n  \"workflow_node.deploy.form.k8s_secret_annotations.help\": \"提示：每行一个键值对，以分号分隔。\",\r\n  \"workflow_node.deploy.form.k8s_secret_annotations.errmsg.invalid\": \"请输入有效的注解键值对\",\r\n  \"workflow_node.deploy.form.k8s_secret_annotations.tooltip\": \"示例：<br><i>environment: production<br>app: nginx</i>\",\r\n  \"workflow_node.deploy.form.k8s_secret_labels.label\": \"Kubernetes Secret 标签（可选）\",\r\n  \"workflow_node.deploy.form.k8s_secret_labels.placeholder\": \"请输入 Kubernetes Secret 标签\",\r\n  \"workflow_node.deploy.form.k8s_secret_labels.help\": \"提示：每行一个键值对，以分号分隔。\",\r\n  \"workflow_node.deploy.form.k8s_secret_labels.errmsg.invalid\": \"请输入有效的标签键值对\",\r\n  \"workflow_node.deploy.form.k8s_secret_labels.tooltip\": \"示例：<br><i>environment: production<br>app: nginx</i>\",\r\n  \"workflow_node.deploy.form.kong.guide\": \"需要 Kong v2.0 或更高版本。\",\r\n  \"workflow_node.deploy.form.kong_resource_type.option.certificate.label\": \"替换指定证书\",\r\n  \"workflow_node.deploy.form.kong_workspace.label\": \"Kong 工作空间（可选）\",\r\n  \"workflow_node.deploy.form.kong_workspace.placeholder\": \"请输入 Kong 工作空间\",\r\n  \"workflow_node.deploy.form.kong_workspace.tooltip\": \"请登录 Kong 控制台查看\",\r\n  \"workflow_node.deploy.form.kong_certificate_id.label\": \"Kong 证书 ID\",\r\n  \"workflow_node.deploy.form.kong_certificate_id.placeholder\": \"请输入 Kong 证书 ID\",\r\n  \"workflow_node.deploy.form.kong_certificate_id.tooltip\": \"请登录 Kong 控制台查看\",\r\n  \"workflow_node.deploy.form.ksyun_cdn_resource_type.option.domain.label\": \"部署到指定加速域名\",\r\n  \"workflow_node.deploy.form.ksyun_cdn_resource_type.option.certificate.label\": \"替换指定证书\",\r\n  \"workflow_node.deploy.form.ksyun_cdn_domain.label\": \"金山云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.ksyun_cdn_domain.placeholder\": \"请输入金山云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.ksyun_cdn_certificate_id.label\": \"金山云 CDN 证书 ID\",\r\n  \"workflow_node.deploy.form.ksyun_cdn_certificate_id.placeholder\": \"请输入金山云 CDN 证书 ID\",\r\n  \"workflow_node.deploy.form.ksyun_cdn_certificate_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cdn.console.ksyun.com/\\\" target=\\\"_blank\\\">https://cdn.console.ksyun.com/</a>\",\r\n  \"workflow_node.deploy.form.lecdn_resource_type.option.certificate.label\": \"替换指定证书\",\r\n  \"workflow_node.deploy.form.lecdn_certificate_id.label\": \"LeCDN 证书 ID\",\r\n  \"workflow_node.deploy.form.lecdn_certificate_id.placeholder\": \"请输入 LeCDN 证书 ID\",\r\n  \"workflow_node.deploy.form.lecdn_certificate_id.tooltip\": \"请登录 LeCDN 控制台查看\",\r\n  \"workflow_node.deploy.form.lecdn_client_id.label\": \"LeCDN 客户 ID（可选）\",\r\n  \"workflow_node.deploy.form.lecdn_client_id.placeholder\": \"请输入 LeCDN 客户 ID\",\r\n  \"workflow_node.deploy.form.lecdn_client_id.tooltip\": \"请登录 LeCDN 控制台查看。<br>使用的是系统管理员的授权信息时必填，需与证书所属客户相同。\",\r\n  \"workflow_node.deploy.form.local.guide\": \"如果你正在使用 Docker 运行 Certimate，「本地」指的是容器内而非宿主机。\",\r\n  \"workflow_node.deploy.form.local_format.label\": \"文件格式\",\r\n  \"workflow_node.deploy.form.local_format.placeholder\": \"请选择文件格式\",\r\n  \"workflow_node.deploy.form.local_format.option.pem.label\": \"PEM 格式（*.pem, *.crt, *.key）\",\r\n  \"workflow_node.deploy.form.local_format.option.pfx.label\": \"PFX 格式（*.pfx, *.p12）\",\r\n  \"workflow_node.deploy.form.local_format.option.jks.label\": \"JKS 格式（*.jks）\",\r\n  \"workflow_node.deploy.form.local_key_path.label\": \"私钥文件保存路径\",\r\n  \"workflow_node.deploy.form.local_key_path.placeholder\": \"请输入私钥文件本地路径\",\r\n  \"workflow_node.deploy.form.local_key_path.help\": \"注意：路径需包含完整的文件名，而不是只有目录。\",\r\n  \"workflow_node.deploy.form.local_cert_path.label\": \"证书文件保存路径\",\r\n  \"workflow_node.deploy.form.local_cert_path.placeholder\": \"请输入证书文件本地路径\",\r\n  \"workflow_node.deploy.form.local_cert_path.help\": \"注意：路径需包含完整的文件名，而不是只有目录。\",\r\n  \"workflow_node.deploy.form.local_fullchaincert_path.label\": \"证书链文件保存路径\",\r\n  \"workflow_node.deploy.form.local_fullchaincert_path.placeholder\": \"请输入证书链文件本地路径\",\r\n  \"workflow_node.deploy.form.local_servercert_path.label\": \"服务器证书文件保存路径（可选）\",\r\n  \"workflow_node.deploy.form.local_servercert_path.placeholder\": \"请输入服务器证书文件本地路径\",\r\n  \"workflow_node.deploy.form.local_servercert_path.help\": \"注意：路径需包含完整的文件名，而不是只有目录。不填写时将不会保存服务器证书。\",\r\n  \"workflow_node.deploy.form.local_intermediacert_path.label\": \"中间证书文件保存路径（可选）\",\r\n  \"workflow_node.deploy.form.local_intermediacert_path.placeholder\": \"请输入中间证书文件本地路径\",\r\n  \"workflow_node.deploy.form.local_intermediacert_path.help\": \"注意：路径需包含完整的文件名，而不是只有目录。不填写时将不会保存中间证书。\",\r\n  \"workflow_node.deploy.form.local_pfx_password.label\": \"PFX 导出密码\",\r\n  \"workflow_node.deploy.form.local_pfx_password.placeholder\": \"请输入 PFX 导出密码\",\r\n  \"workflow_node.deploy.form.local_pfx_password.tooltip\": \"这是什么？请参阅 <a href=\\\"https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/personal-information-exchange---pfx--files\\\" target=\\\"_blank\\\">https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/personal-information-exchange---pfx--files</a>\",\r\n  \"workflow_node.deploy.form.local_jks_alias.label\": \"JKS 别名\",\r\n  \"workflow_node.deploy.form.local_jks_alias.placeholder\": \"请输入 JKS 别名\",\r\n  \"workflow_node.deploy.form.local_jks_alias.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.local_jks_keypass.label\": \"JKS 私钥访问口令\",\r\n  \"workflow_node.deploy.form.local_jks_keypass.placeholder\": \"请输入 JKS 私钥访问口令\",\r\n  \"workflow_node.deploy.form.local_jks_keypass.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.local_jks_storepass.label\": \"JKS 密钥库存储口令\",\r\n  \"workflow_node.deploy.form.local_jks_storepass.placeholder\": \"请输入 JKS 密钥库存储口令\",\r\n  \"workflow_node.deploy.form.local_jks_storepass.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.local_shell_env.label\": \"命令执行环境\",\r\n  \"workflow_node.deploy.form.local_shell_env.placeholder\": \"请选择命令执行环境\",\r\n  \"workflow_node.deploy.form.local_shell_env.option.sh.label\": \"POSIX Bash（Linux / macOS）\",\r\n  \"workflow_node.deploy.form.local_shell_env.option.cmd.label\": \"CMD（Windows）\",\r\n  \"workflow_node.deploy.form.local_shell_env.option.powershell.label\": \"PowerShell（Windows）\",\r\n  \"workflow_node.deploy.form.local_pre_command.label\": \"前置命令（可选）\",\r\n  \"workflow_node.deploy.form.local_pre_command.placeholder\": \"请输入保存文件前执行的命令\",\r\n  \"workflow_node.deploy.form.local_post_command.label\": \"后置命令（可选）\",\r\n  \"workflow_node.deploy.form.local_post_command.placeholder\": \"请输入保存文件后执行的命令\",\r\n  \"workflow_node.deploy.form.local_preset_scripts.sh_backup_files\": \"POSIX Bash - 备份原证书文件\",\r\n  \"workflow_node.deploy.form.local_preset_scripts.ps_backup_files\": \"PowerShell - 备份原证书文件\",\r\n  \"workflow_node.deploy.form.local_preset_scripts.sh_reload_nginx\": \"POSIX Bash - 重启 nginx 进程\",\r\n  \"workflow_node.deploy.form.local_preset_scripts.ps_binding_iis\": \"PowerShell - 导入并绑定到 IIS\",\r\n  \"workflow_node.deploy.form.local_preset_scripts.ps_binding_netsh\": \"PowerShell - 导入并绑定到 netsh\",\r\n  \"workflow_node.deploy.form.local_preset_scripts.ps_binding_rdp\": \"PowerShell - 导入并绑定到 RDP\",\r\n  \"workflow_node.deploy.form.mohua_mvh_host_id.label\": \"嘿华云虚拟主机 ID\",\r\n  \"workflow_node.deploy.form.mohua_mvh_host_id.placeholder\": \"请输入嘿华云虚拟主机 ID\",\r\n  \"workflow_node.deploy.form.mohua_mvh_host_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.mhjz1.cn/service?groupid=328\\\" target=\\\"_blank\\\">https://cloud.mhjz1.cn/service?groupid=328</a>\",\r\n  \"workflow_node.deploy.form.mohua_mvh_domain_id.label\": \"嘿华云虚拟主机域名 ID\",\r\n  \"workflow_node.deploy.form.mohua_mvh_domain_id.placeholder\": \"请输入嘿华云虚拟主机域名 ID\",\r\n  \"workflow_node.deploy.form.mohua_mvh_domain_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.mhjz1.cn/service?groupid=328\\\" target=\\\"_blank\\\">https://cloud.mhjz1.cn/service?groupid=328</a>\",\r\n  \"workflow_node.deploy.form.netlify_resource_type.option.website.label\": \"部署到指定网站\",\r\n  \"workflow_node.deploy.form.netlify_site_id.label\": \"Netlify 网站 ID\",\r\n  \"workflow_node.deploy.form.netlify_site_id.placeholder\": \"请输入 netlify 网站 ID\",\r\n  \"workflow_node.deploy.form.netlify_site_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.netlify.com/api/get-started/#get-site\\\" target=\\\"_blank\\\">https://docs.netlify.com/api/get-started/#get-site</a>\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_resource_type.option.host.label\": \"部署到指定主机\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_resource_type.option.certificate.label\": \"替换指定证书\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_match_pattern.label\": \"主机匹配模式\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_match_pattern.placeholder\": \"请选择部署主机匹配模式\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_match_pattern.option.specified.label\": \"指定 ID\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_match_pattern.option.certsan.label\": \"根据证书自动匹配\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_type.label\": \"NPM 主机类型\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_type.placeholder\": \"请输入 NPM 主机类型\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_type.option.proxy.label\": \"代理服务\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_type.option.redirection.label\": \"重定向主机\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_type.option.stream.label\": \"端口转发\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_type.option.dead.label\": \"错误页面\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_id.label\": \"NPM 主机 ID\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_id.placeholder\": \"请输入 NPM 主机 ID\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_host_id.tooltip\": \"请登录 NPM 面板查看\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_certificate_id.label\": \"NPM 证书 ID\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_certificate_id.placeholder\": \"请输入 NPM 证书 ID\",\r\n  \"workflow_node.deploy.form.nginxproxymanager_certificate_id.tooltip\": \"请登录 NPM 面板查看\",\r\n  \"workflow_node.deploy.form.proxmoxve.guide\": \"需要 Proxmox VE v5.2 或更高版本。\",\r\n  \"workflow_node.deploy.form.proxmoxve_node_name.label\": \"Proxmox VE 集群节点名称\",\r\n  \"workflow_node.deploy.form.proxmoxve_node_name.placeholder\": \"请输入 Proxmox VE 集群节点名称\",\r\n  \"workflow_node.deploy.form.proxmoxve_auto_restart.label\": \"部署后自动重启 Proxmox VE 服务\",\r\n  \"workflow_node.deploy.form.qiniu_cdn_domain.label\": \"七牛云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.qiniu_cdn_domain.placeholder\": \"请输入七牛云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.qiniu_kodo_bucket.label\": \"七牛云对象存储桶名\",\r\n  \"workflow_node.deploy.form.qiniu_kodo_bucket.placeholder\": \"请输入七牛云对象存储桶名\",\r\n  \"workflow_node.deploy.form.qiniu_kodo_domain.label\": \"七牛云对象存储自定义域名\",\r\n  \"workflow_node.deploy.form.qiniu_kodo_domain.placeholder\": \"请输入七牛云对象存储自定义域名\",\r\n  \"workflow_node.deploy.form.qiniu_pili_hub.label\": \"七牛云视频直播空间名\",\r\n  \"workflow_node.deploy.form.qiniu_pili_hub.placeholder\": \"请输入七牛云视频直播空间名\",\r\n  \"workflow_node.deploy.form.qiniu_pili_hub.tooltip\": \"这是什么？请参阅 <a href=\\\"https://portal.qiniu.com/hub\\\" target=\\\"_blank\\\">https://portal.qiniu.com/hub</a>\",\r\n  \"workflow_node.deploy.form.qiniu_pili_domain.label\": \"七牛云视频直播流域名\",\r\n  \"workflow_node.deploy.form.qiniu_pili_domain.placeholder\": \"请输入七牛云视频直播流域名\",\r\n  \"workflow_node.deploy.form.rainyun_rcdn_instance_id.label\": \"雨云 RCDN 实例 ID\",\r\n  \"workflow_node.deploy.form.rainyun_rcdn_instance_id.placeholder\": \"请输入雨云 RCDN 实例 ID\",\r\n  \"workflow_node.deploy.form.rainyun_rcdn_instance_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://app.rainyun.com/apps/rcdn/list\\\" target=\\\"_blank\\\">https://app.rainyun.com/apps/rcdn/list</a>\",\r\n  \"workflow_node.deploy.form.rainyun_rcdn_domain.label\": \"雨云 RCDN 加速域名\",\r\n  \"workflow_node.deploy.form.rainyun_rcdn_domain.placeholder\": \"请输入雨云 RCDN 加速域名\",\r\n  \"workflow_node.deploy.form.rainyun_sslcenter_certificate_id.label\": \"雨云 RCDN 证书 ID\",\r\n  \"workflow_node.deploy.form.rainyun_sslcenter_certificate_id.placeholder\": \"请输入雨云 RCDN 证书 ID\",\r\n  \"workflow_node.deploy.form.rainyun_sslcenter_certificate_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://app.rainyun.com/apps/ssl/list\\\" target=\\\"_blank\\\">https://app.rainyun.com/apps/ssl/list</a>\",\r\n  \"workflow_node.deploy.form.rainyun_sslcenter_certificate_id.help\": \"提示：不填写时，将上传新证书；否则，将替换原证书。\",\r\n  \"workflow_node.deploy.form.ratpanel.guide\": \"需要耗子面板 v2.5 或更高版本。\",\r\n  \"workflow_node.deploy.form.ratpanel_resource_type.option.website.label\": \"部署到指定网站\",\r\n  \"workflow_node.deploy.form.ratpanel_resource_type.option.certificate.label\": \"替换指定证书\",\r\n  \"workflow_node.deploy.form.ratpanel_site_names.label\": \"耗子面板网站名称\",\r\n  \"workflow_node.deploy.form.ratpanel_site_names.placeholder\": \"请输入耗子面板网站名称（多个值请用半角分号隔开）\",\r\n  \"workflow_node.deploy.form.ratpanel_site_names.errmsg.invalid\": \"请输入正确的耗子面板网站名称\",\r\n  \"workflow_node.deploy.form.ratpanel_site_names.help\": \"提示：支持多个网站名称，以半角分号隔开。\",\r\n  \"workflow_node.deploy.form.ratpanel_site_names.tooltip\": \"请登录耗子面板查看\",\r\n  \"workflow_node.deploy.form.ratpanel_site_names.multiple_input_modal.title\": \"修改耗子面板网站名称\",\r\n  \"workflow_node.deploy.form.ratpanel_site_names.multiple_input_modal.placeholder\": \"请输入耗子面板网站名称\",\r\n  \"workflow_node.deploy.form.ratpanel_certificate_id.label\": \"耗子面板证书 ID\",\r\n  \"workflow_node.deploy.form.ratpanel_certificate_id.placeholder\": \"请输入耗子面板证书 ID\",\r\n  \"workflow_node.deploy.form.ratpanel_certificate_id.tooltip\": \"请登录耗子面板查看\",\r\n  \"workflow_node.deploy.form.s3_region.label\": \"对象存储区域\",\r\n  \"workflow_node.deploy.form.s3_region.placeholder\": \"请输入对象存储区域\",\r\n  \"workflow_node.deploy.form.s3_bucket.label\": \"对象存储桶名\",\r\n  \"workflow_node.deploy.form.s3_bucket.placeholder\": \"请输入对象存储桶名\",\r\n  \"workflow_node.deploy.form.s3_format.label\": \"文件格式\",\r\n  \"workflow_node.deploy.form.s3_format.placeholder\": \"请选择文件格式\",\r\n  \"workflow_node.deploy.form.s3_format.option.pem.label\": \"PEM 格式（*.pem, *.crt, *.key）\",\r\n  \"workflow_node.deploy.form.s3_format.option.pfx.label\": \"PFX 格式（*.pfx, *.p12）\",\r\n  \"workflow_node.deploy.form.s3_format.option.jks.label\": \"JKS 格式（*.jks）\",\r\n  \"workflow_node.deploy.form.s3_key_object_key.label\": \"私钥文件对象键\",\r\n  \"workflow_node.deploy.form.s3_key_object_key.placeholder\": \"请输入私钥文件对象键\",\r\n  \"workflow_node.deploy.form.s3_cert_object_key.label\": \"证书文件对象键\",\r\n  \"workflow_node.deploy.form.s3_cert_object_key.placeholder\": \"请输入证书文件对象键\",\r\n  \"workflow_node.deploy.form.s3_fullchaincert_object_key.label\": \"证书链文件对象键\",\r\n  \"workflow_node.deploy.form.s3_fullchaincert_object_key.placeholder\": \"请输入证书链文件对象键\",\r\n  \"workflow_node.deploy.form.s3_servercert_object_key.label\": \"服务器证书文件对象键（可选）\",\r\n  \"workflow_node.deploy.form.s3_servercert_object_key.placeholder\": \"请输入服务器证书文件对象键\",\r\n  \"workflow_node.deploy.form.s3_servercert_object_key.help\": \"注意：不填写时将不会上传服务器证书。\",\r\n  \"workflow_node.deploy.form.s3_intermediacert_object_key.label\": \"中间证书文件对象键（可选）\",\r\n  \"workflow_node.deploy.form.s3_intermediacert_object_key.placeholder\": \"请输入中间证书文件对象键\",\r\n  \"workflow_node.deploy.form.s3_intermediacert_object_key.help\": \"注意：不填写时将不会上传中间证书。\",\r\n  \"workflow_node.deploy.form.s3_pfx_password.label\": \"PFX 导出密码\",\r\n  \"workflow_node.deploy.form.s3_pfx_password.placeholder\": \"请输入 PFX 导出密码\",\r\n  \"workflow_node.deploy.form.s3_pfx_password.tooltip\": \"这是什么？请参阅 <a href=\\\"https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/personal-information-exchange---pfx--files\\\" target=\\\"_blank\\\">https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/personal-information-exchange---pfx--files</a>\",\r\n  \"workflow_node.deploy.form.s3_jks_alias.label\": \"JKS 别名\",\r\n  \"workflow_node.deploy.form.s3_jks_alias.placeholder\": \"请输入 JKS 别名\",\r\n  \"workflow_node.deploy.form.s3_jks_alias.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.s3_jks_keypass.label\": \"JKS 私钥访问口令\",\r\n  \"workflow_node.deploy.form.s3_jks_keypass.placeholder\": \"请输入 JKS 私钥访问口令\",\r\n  \"workflow_node.deploy.form.s3_jks_keypass.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.s3_jks_storepass.label\": \"JKS 密钥库存储口令\",\r\n  \"workflow_node.deploy.form.s3_jks_storepass.placeholder\": \"请输入 JKS 密钥库存储口令\",\r\n  \"workflow_node.deploy.form.s3_jks_storepass.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.safeline.guide\": \"需要雷池 v6.6 或更高版本。\",\r\n  \"workflow_node.deploy.form.safeline_resource_type.option.certificate.label\": \"替换指定证书\",\r\n  \"workflow_node.deploy.form.safeline_certificate_id.label\": \"雷池证书 ID\",\r\n  \"workflow_node.deploy.form.safeline_certificate_id.placeholder\": \"请输入雷池证书 ID\",\r\n  \"workflow_node.deploy.form.safeline_certificate_id.tooltip\": \"请登录雷池控制台查看\",\r\n  \"workflow_node.deploy.form.ssh_format.label\": \"文件格式\",\r\n  \"workflow_node.deploy.form.ssh_format.placeholder\": \"请选择文件格式\",\r\n  \"workflow_node.deploy.form.ssh_format.option.pem.label\": \"PEM 格式（*.pem, *.crt, *.key）\",\r\n  \"workflow_node.deploy.form.ssh_format.option.pfx.label\": \"PFX 格式（*.pfx, *.p12）\",\r\n  \"workflow_node.deploy.form.ssh_format.option.jks.label\": \"JKS 格式（*.jks）\",\r\n  \"workflow_node.deploy.form.ssh_key_path.label\": \"私钥文件保存路径\",\r\n  \"workflow_node.deploy.form.ssh_key_path.placeholder\": \"请输入私钥文件远程路径\",\r\n  \"workflow_node.deploy.form.ssh_key_path.help\": \"注意：路径需包含完整的文件名，而不是只有目录。\",\r\n  \"workflow_node.deploy.form.ssh_cert_path.label\": \"证书文件保存路径\",\r\n  \"workflow_node.deploy.form.ssh_cert_path.placeholder\": \"请输入证书文件远程路径\",\r\n  \"workflow_node.deploy.form.ssh_cert_path.help\": \"注意：路径需包含完整的文件名，而不是只有目录。\",\r\n  \"workflow_node.deploy.form.ssh_fullchaincert_path.label\": \"证书链文件保存路径\",\r\n  \"workflow_node.deploy.form.ssh_fullchaincert_path.placeholder\": \"请输入证书链文件远程路径\",\r\n  \"workflow_node.deploy.form.ssh_servercert_path.label\": \"服务器证书文件保存路径（可选）\",\r\n  \"workflow_node.deploy.form.ssh_servercert_path.placeholder\": \"请输入服务器证书文件远程路径\",\r\n  \"workflow_node.deploy.form.ssh_servercert_path.help\": \"注意：路径需包含完整的文件名，而不是只有目录。不填写时将不上传服务器证书。\",\r\n  \"workflow_node.deploy.form.ssh_intermediacert_path.label\": \"中间证书文件保存路径（可选）\",\r\n  \"workflow_node.deploy.form.ssh_intermediacert_path.placeholder\": \"请输入中间证书文件远程路径\",\r\n  \"workflow_node.deploy.form.ssh_intermediacert_path.help\": \"注意：路径需包含完整的文件名，而不是只有目录。不填写时将不上传中间证书。\",\r\n  \"workflow_node.deploy.form.ssh_pfx_password.label\": \"PFX 导出密码\",\r\n  \"workflow_node.deploy.form.ssh_pfx_password.placeholder\": \"请输入 PFX 导出密码\",\r\n  \"workflow_node.deploy.form.ssh_pfx_password.tooltip\": \"这是什么？请参阅 <a href=\\\"https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/personal-information-exchange---pfx--files\\\" target=\\\"_blank\\\">https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/personal-information-exchange---pfx--files</a>\",\r\n  \"workflow_node.deploy.form.ssh_jks_alias.label\": \"JKS 别名\",\r\n  \"workflow_node.deploy.form.ssh_jks_alias.placeholder\": \"请输入 JKS 别名\",\r\n  \"workflow_node.deploy.form.ssh_jks_alias.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.ssh_jks_keypass.label\": \"JKS 私钥访问口令\",\r\n  \"workflow_node.deploy.form.ssh_jks_keypass.placeholder\": \"请输入 JKS 私钥访问口令\",\r\n  \"workflow_node.deploy.form.ssh_jks_keypass.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.ssh_jks_storepass.label\": \"JKS 密钥库存储口令\",\r\n  \"workflow_node.deploy.form.ssh_jks_storepass.placeholder\": \"请输入 JKS 密钥库存储口令\",\r\n  \"workflow_node.deploy.form.ssh_jks_storepass.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\\\" target=\\\"_blank\\\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>\",\r\n  \"workflow_node.deploy.form.ssh_pre_command.label\": \"前置命令（可选）\",\r\n  \"workflow_node.deploy.form.ssh_pre_command.placeholder\": \"请输入上传文件前执行的命令\",\r\n  \"workflow_node.deploy.form.ssh_post_command.label\": \"后置命令（可选）\",\r\n  \"workflow_node.deploy.form.ssh_post_command.placeholder\": \"请输入上传文件后执行的命令\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.sh_backup_files\": \"POSIX Bash - 备份原证书文件\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.ps_backup_files\": \"PowerShell - 备份原证书文件\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.sh_reload_nginx\": \"POSIX Bash - 重启 nginx 进程\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.sh_replace_synologydsm_ssl\": \"POSIX Bash - 替换群晖 DSM 证书\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.sh_replace_fnos_ssl\": \"POSIX Bash - 替换飞牛 fnOS 证书\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.sh_replace_qnap_ssl\": \"POSIX Bash - 替换威联通 QNAP 证书\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.ps_binding_iis\": \"PowerShell - 导入并绑定到 IIS\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.ps_binding_netsh\": \"PowerShell - 导入并绑定到 netsh\",\r\n  \"workflow_node.deploy.form.ssh_preset_scripts.ps_binding_rdp\": \"PowerShell - 导入并绑定到 RDP\",\r\n  \"workflow_node.deploy.form.ssh_use_scp.label\": \"回退使用 SCP\",\r\n  \"workflow_node.deploy.form.ssh_use_scp.tooltip\": \"如果你的远程服务器不支持 SFTP，请勾选此选项回退为 SCP。\",\r\n  \"workflow_node.deploy.form.synologydsm.guide\": \"需要群晖 DSM v6.0 或更高版本。\",\r\n  \"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.label\": \"群晖 DSM 原证书 ID 或描述（可选）\",\r\n  \"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.placeholder\": \"请输入群晖 DSM 原证书 ID 或描述\",\r\n  \"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.help\": \"提示：不填写时，将上传新证书；否则，将替换原证书。\",\r\n  \"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.tooltip\": \"请登录群晖 DSM 面板查看<br><br>（目前需要在「控制面板 -> 安全性 -> 证书」页面，并打开浏览器开发者工具抓取网络请求后得到证书 ID）\",\r\n  \"workflow_node.deploy.form.synologydsm_is_default.label\": \"设为默认证书并应用到所有 DSM 服务\",\r\n  \"workflow_node.deploy.form.tencentcloud_cdn_endpoint.label\": \"腾讯云接口端点（可选）\",\r\n  \"workflow_node.deploy.form.tencentcloud_cdn_endpoint.placeholder\": \"请输入腾讯云 CDN 接口端点（例如：cdn.tencentcloudapi.com）\",\r\n  \"workflow_node.deploy.form.tencentcloud_cdn_endpoint.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/228/30976\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/228/30976</a><br>国际站用户请填写 <em>cdn.intl.tencentcloudapi.com</em>。\",\r\n  \"workflow_node.deploy.form.tencentcloud_cdn_domain.label\": \"腾讯云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_cdn_domain.placeholder\": \"请输入腾讯云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_endpoint.label\": \"腾讯云接口端点（可选）\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_endpoint.placeholder\": \"请输入腾讯云 CLB 接口端点（例如：clb.tencentcloudapi.com）\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_endpoint.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/214/30669\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/214/30669</a><br>国际站用户请填写 <em>clb.intl.tencentcloudapi.com</em>。\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_region.label\": \"腾讯云服务地域\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_region.placeholder\": \"请输入腾讯云 CLB 服务地域（例如：ap-guangzhou）\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/214/33415\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/214/33415</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_resource_type.option.loadbalancer.label\": \"部署到指定实例下的全部 HTTPS/TCPSSL/QUIC 监听器\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_resource_type.option.listener.label\": \"部署到指定 HTTPS/TCPSSL/QUIC 监听器\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_resource_type.option.ruledomain.label\": \"部署到指定七层监听转发规则域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_loadbalancer_id.label\": \"腾讯云 CLB 实例 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_loadbalancer_id.placeholder\": \"请输入腾讯云 CLB 实例 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_loadbalancer_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.cloud.tencent.com/clb\\\" target=\\\"_blank\\\">https://console.cloud.tencent.com/clb</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_listener_id.label\": \"腾讯云 CLB 监听器 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_listener_id.placeholder\": \"请输入腾讯云 CLB 监听器 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_listener_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.cloud.tencent.com/clb\\\" target=\\\"_blank\\\">https://console.cloud.tencent.com/clb</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_ruledomain.label\": \"腾讯云 CLB 七层转发规则域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_clb_ruledomain.placeholder\": \"请输入腾讯云 CLB 七层转发规则域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_cos_region.label\": \"腾讯云服务地域\",\r\n  \"workflow_node.deploy.form.tencentcloud_cos_region.placeholder\": \"请输入腾讯云 COS 服务地域（例如：ap-guangzhou）\",\r\n  \"workflow_node.deploy.form.tencentcloud_cos_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/436/6224\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/436/6224</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_cos_bucket.label\": \"腾讯云 COS 存储桶名\",\r\n  \"workflow_node.deploy.form.tencentcloud_cos_bucket.placeholder\": \"请输入腾讯云 COS 存储桶名\",\r\n  \"workflow_node.deploy.form.tencentcloud_cos_domain.label\": \"腾讯云 COS 自定义域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_cos_domain.placeholder\": \"请输入腾讯云 COS 自定义域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_css_endpoint.label\": \"腾讯云接口端点（可选）\",\r\n  \"workflow_node.deploy.form.tencentcloud_css_endpoint.placeholder\": \"请输入腾讯云云直播接口端点（例如：live.tencentcloudapi.com）\",\r\n  \"workflow_node.deploy.form.tencentcloud_css_endpoint.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/267/20458\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/267/20458</a><br>国际站用户请填写 <em>live.intl.tencentcloudapi.com</em>。\",\r\n  \"workflow_node.deploy.form.tencentcloud_css_domain.label\": \"腾讯云云直播播放域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_css_domain.placeholder\": \"请输入腾讯云云直播播放域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_ecdn_endpoint.label\": \"腾讯云接口端点（可选）\",\r\n  \"workflow_node.deploy.form.tencentcloud_ecdn_endpoint.placeholder\": \"请输入腾讯云 ECDN 接口端点（例如：cdn.tencentcloudapi.com）\",\r\n  \"workflow_node.deploy.form.tencentcloud_ecdn_endpoint.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/214/30669\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/214/30669</a><br>国际站用户请填写 <em>cdn.intl.tencentcloudapi.com</em>。\",\r\n  \"workflow_node.deploy.form.tencentcloud_ecdn_domain.label\": \"腾讯云 ECDN 加速域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_ecdn_domain.placeholder\": \"请输入腾讯云 ECDN 加速域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_endpoint.label\": \"腾讯云接口端点（可选）\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_endpoint.placeholder\": \"请输入腾讯云 EdgeOne 接口端点（例如：teo.tencentcloudapi.com）\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_endpoint.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/1552/80723\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/1552/80723</a><br>国际站用户请填写 <em>teo.intl.tencentcloudapi.com</em>。\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_zone_id.label\": \"腾讯云 EdgeOne 站点 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder\": \"请输入腾讯云 EdgeOne 站点 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_zone_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.cloud.tencent.com/edgeone\\\" target=\\\"_blank\\\">https://console.cloud.tencent.com/edgeone</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_domains.label\": \"腾讯云 EdgeOne 加速域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_domains.placeholder\": \"请输入腾讯云 EdgeOne 加速域名（多个值请用半角分号隔开）\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_domains.help\": \"提示：支持多个域名，以半角分号隔开。\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.title\": \"修改腾讯云 EdgeOne 加速域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.placeholder\": \"请输入腾讯云 EdgeOne 加速域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.label\": \"多证书模式\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.help\": \"提示：每个域名最多支持一个 RSA 证书、一个 ECC 证书。\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.suffix\": \"保留与待部署证书算法不一致的其他有效证书。\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.on\": \"\",\r\n  \"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.off\": \"不\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_endpoint.label\": \"腾讯云接口端点（可选）\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_endpoint.placeholder\": \"请输入腾讯云 GAAP 接口端点（例如：gaap.tencentcloudapi.com）\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_endpoint.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/608/36934\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/608/36934</a><br>国际站用户请填写 <em>gaap.intl.tencentcloudapi.com</em>。\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_resource_type.option.listener.label\": \"部署到指定监听器\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_proxy_id.label\": \"腾讯云 GAAP 通道 ID（可选）\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_proxy_id.placeholder\": \"请输入腾讯云 GAAP 通道 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_proxy_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.cloud.tencent.com/gaap\\\" target=\\\"_blank\\\">https://console.cloud.tencent.com/gaap</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_listener_id.label\": \"腾讯云 GAAP 监听器 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_listener_id.placeholder\": \"请输入腾讯云 GAAP 监听器 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_gaap_listener_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.cloud.tencent.com/gaap\\\" target=\\\"_blank\\\">https://console.cloud.tencent.com/gaap</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_scf_endpoint.label\": \"腾讯云接口端点（可选）\",\r\n  \"workflow_node.deploy.form.tencentcloud_scf_endpoint.placeholder\": \"请输入腾讯云 SCF 接口端点（例如：scf.tencentcloudapi.com）\",\r\n  \"workflow_node.deploy.form.tencentcloud_scf_endpoint.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/583/17237\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/583/17237</a><br>国际站用户请填写 <em>scf.intl.tencentcloudapi.com</em>。\",\r\n  \"workflow_node.deploy.form.tencentcloud_scf_region.label\": \"腾讯云服务地域\",\r\n  \"workflow_node.deploy.form.tencentcloud_scf_region.placeholder\": \"输入腾讯云 SCF 服务地域（例如：ap-guangzhou）\",\r\n  \"workflow_node.deploy.form.tencentcloud_scf_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/583/17299\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/583/17299</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_scf_domain.label\": \"腾讯云 SCF 自定义域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_scf_domain.placeholder\": \"输入腾讯云 SCF 自定义域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssl_endpoint.label\": \"腾讯云接口端点（可选）\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssl_endpoint.placeholder\": \"请输入腾讯云 SSL 接口端点（例如：ssl.tencentcloudapi.com）\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssl_endpoint.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/400/41659\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/400/41659</a><br>国际站用户请填写 <em>ssl.intl.tencentcloudapi.com</em>。\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy.guide\": \"将通过腾讯云 OpenAPI <em>DeployCertificateInstance</em> 接口创建异步部署任务。此部署目标若执行成功仅代表已创建部署任务，实际部署结果需要你自行前往腾讯云控制台查询。\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_endpoint.label\": \"腾讯云接口端点（可选）\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_endpoint.placeholder\": \"请输入腾讯云 SSL 接口端点（例如：ssl.tencentcloudapi.com）\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_endpoint.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/400/41659\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/400/41659</a><br>国际站用户请填写 <em>ssl.intl.tencentcloudapi.com</em>。\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_region.label\": \"腾讯云服务地域\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_region.placeholder\": \"请输入腾讯云云产品服务地域（例如：ap-guangzhou）\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/400/41659\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/400/41659</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_product.label\": \"腾讯云云产品资源类型\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_product.placeholder\": \"请输入腾讯云产品资源类型\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_product.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/400/91667\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/400/91667</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.label\": \"腾讯云云产品资源 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.placeholder\": \"请输入腾讯云云产品资源 ID（多个值请用半角分号隔开）\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.errmsg.invalid\": \"请输入正确的腾讯云云产品资源 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.help\": \"提示：支持多个 ID，以半角分号隔开。\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/400/91667\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/400/91667</a><br>注意与各产品本身的实例 ID 区分。\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.multiple_input_modal.title\": \"修改腾讯云云产品资源 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.multiple_input_modal.placeholder\": \"请输入腾讯云云产品资源 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate.guide\": \"将通过腾讯云 OpenAPI <em>UpdateCertificateInstance</em> 或 <em>UploadUpdateCertificateInstance</em> 接口创建异步部署任务。此部署目标若执行成功仅代表已创建部署任务，实际部署结果需要你自行前往腾讯云控制台查询。\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_endpoint.label\": \"腾讯云接口端点（可选）\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_endpoint.placeholder\": \"请输入腾讯云 SSL 接口端点（例如：ssl.tencentcloudapi.com）\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_endpoint.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/400/41659\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/400/41659</a><br>国际站用户请填写 <em>ssl.intl.tencentcloudapi.com</em>。\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_certificate_id.label\": \"腾讯云原证书 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_certificate_id.placeholder\": \"请输入腾讯云原证书 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_certificate_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.cloud.tencent.com/certoverview\\\" target=\\\"_blank\\\">https://console.cloud.tencent.com/certoverview</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.label\": \"腾讯云云产品资源类型\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.placeholder\": \"请输入腾讯云云产品资源类型（多个值请用半角分号隔开）\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.help\": \"提示：支持多个类型，以半角分号隔开。\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/400/91649\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/400/91649</a> 或 <a href=\\\"https://cloud.tencent.com/document/product/400/119791\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/400/119791</a><br>注意，这两个接口的所支持的云产品资源类型有所不同，具体请查看腾讯云官方文档。\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.multiple_input_modal.title\": \"修改腾讯云云产品资源类型\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.multiple_input_modal.placeholder\": \"请输入腾讯云云产品资源类型\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.label\": \"腾讯云云产品部署地域（可选）\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.placeholder\": \"请输入腾讯云云产品部署地域（多个值请用半角分号隔开）\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.help\": \"提示：支持多个地域，以半角分号隔开。\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/400/91649\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/400/91649</a> 或 <a href=\\\"https://cloud.tencent.com/document/product/400/119791\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/400/119791</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.multiple_input_modal.title\": \"修改腾讯云云产品部署地域\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.multiple_input_modal.placeholder\": \"请输入腾讯云云产品部署地域\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_is_replaced.label\": \"是否更新原证书（即证书 ID 保持不变）\",\r\n  \"workflow_node.deploy.form.tencentcloud_sslupdate_is_replaced.tooltip\": \"不勾选时，将调用腾讯云 OpenAPI <em>UpdateCertificateInstance</em> 接口；否则，将调用腾讯云 OpenAPI <em>UploadUpdateCertificateInstance</em> 接口。\",\r\n  \"workflow_node.deploy.form.tencentcloud_vod_endpoint.label\": \"腾讯云接口端点（可选）\",\r\n  \"workflow_node.deploy.form.tencentcloud_vod_endpoint.placeholder\": \"请输入腾讯云云点播接口端点（例如：vod.tencentcloudapi.com）\",\r\n  \"workflow_node.deploy.form.tencentcloud_vod_endpoint.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/266/31755\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/266/31755</a><br>国际站用户请填写 <em>vod.intl.tencentcloudapi.com</em>。\",\r\n  \"workflow_node.deploy.form.tencentcloud_vod_sub_app_id.label\": \"腾讯云云点播应用 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_vod_sub_app_id.placeholder\": \"请输入腾讯云云点播应用 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_vod_sub_app_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.cloud.tencent.com/vod\\\" target=\\\"_blank\\\">https://console.cloud.tencent.com/vod</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_vod_domain.label\": \"腾讯云云点播加速域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_vod_domain.placeholder\": \"请输入腾讯云云点播加速域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_endpoint.label\": \"腾讯云接口端点（可选）\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_endpoint.placeholder\": \"请输入腾讯云 WAF 接口端点（例如：waf.tencentcloudapi.com）\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_endpoint.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/627/53611\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/627/53611</a><br>国际站用户请填写 <em>waf.intl.tencentcloudapi.com</em>。\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_region.label\": \"腾讯云服务地域\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_region.placeholder\": \"请输入腾讯云 WAF 服务地域（例如：ap-guangzhou）\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cloud.tencent.com/document/product/627/47525\\\" target=\\\"_blank\\\">https://cloud.tencent.com/document/product/627/47525</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_domain.label\": \"腾讯云 WAF 防护域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_domain.placeholder\": \"请输入腾讯云 WAF 防护域名\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_domain_id.label\": \"腾讯云 WAF 域名 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_domain_id.placeholder\": \"请输入腾讯云 WAF 域名 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_domain_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.cloud.tencent.com/waf\\\" target=\\\"_blank\\\">https://console.cloud.tencent.com/waf</a>\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_instance_id.label\": \"腾讯云 WAF 实例 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_instance_id.placeholder\": \"请输入腾讯云 WAF 实例 ID\",\r\n  \"workflow_node.deploy.form.tencentcloud_waf_instance_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.cloud.tencent.com/waf\\\" target=\\\"_blank\\\">https://console.cloud.tencent.com/waf</a>\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_region.label\": \"优刻得服务地域\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_region.placeholder\": \"优刻得 UALB 服务地域（例如：cn-bj2）\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.ucloud.cn/api/summary/regionlist\\\" target=\\\"_blank\\\">https://docs.ucloud.cn/api/summary/regionlist</a>\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_resource_type.option.loadbalancer.label\": \"部署到指定负载均衡器下的全部 HTTPS 监听器\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_resource_type.option.listener.label\": \"部署到指定 HTTPS 监听器\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_loadbalancer_id.label\": \"优刻得 UALB 负载均衡器实例 ID\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_loadbalancer_id.placeholder\": \"请输入优刻得 UALB 负载均衡器实例 ID\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_loadbalancer_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.ucloud.cn/ulb/alb\\\" target=\\\"_blank\\\">https://console.ucloud.cn/ulb/alb</a>\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_listener_id.label\": \"优刻得 UALB 监听器 ID\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_listener_id.placeholder\": \"请输入优刻得 UALB 监听器 ID\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_listener_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.ucloud.cn/ulb/alb\\\" target=\\\"_blank\\\">https://console.ucloud.cn/ulb/alb</a>\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_snidomain.label\": \"优刻得 UALB 扩展域名（可选）\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_snidomain.placeholder\": \"请输入优刻得 UALB 扩展域名\",\r\n  \"workflow_node.deploy.form.ucloud_ualb_snidomain.help\": \"提示：不填写时，将替换监听器的默认证书；否则，将替换扩展域名证书。\",\r\n  \"workflow_node.deploy.form.ucloud_ucdn_domain_id.label\": \"优刻得 UCDN 域名 ID\",\r\n  \"workflow_node.deploy.form.ucloud_ucdn_domain_id.placeholder\": \"请输入优刻得 UCDN 域名 ID\",\r\n  \"workflow_node.deploy.form.ucloud_ucdn_domain_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.ucloud.cn/ucdn\\\" target=\\\"_blank\\\">https://console.ucloud.cn/ucdn</a>\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_region.label\": \"优刻得服务地域\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_region.placeholder\": \"优刻得 UCLB 服务地域（例如：cn-bj2）\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.ucloud.cn/api/summary/regionlist\\\" target=\\\"_blank\\\">https://docs.ucloud.cn/api/summary/regionlist</a>\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_resource_type.option.loadbalancer.label\": \"部署到指定负载均衡器下的全部 HTTPS VServer\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_resource_type.option.vserver.label\": \"部署到指定 HTTPS VServer\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_loadbalancer_id.label\": \"优刻得 UCLB 负载均衡器实例 ID\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_loadbalancer_id.placeholder\": \"请输入优刻得 UCLB 负载均衡器实例 ID\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_loadbalancer_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.ucloud.cn/ulb/ulb\\\" target=\\\"_blank\\\">https://console.ucloud.cn/ulb/ulb</a>\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_vserver_id.label\": \"优刻得 UCLB VServer ID\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_vserver_id.placeholder\": \"请输入优刻得 UCLB VServer ID\",\r\n  \"workflow_node.deploy.form.ucloud_uclb_vserver_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.ucloud.cn/ulb/ulb\\\" target=\\\"_blank\\\">https://console.ucloud.cn/ulb/ulb</a>\",\r\n  \"workflow_node.deploy.form.ucloud_upathx_accelerator_id.label\": \"优刻得 UPathX 加速器实例 ID\",\r\n  \"workflow_node.deploy.form.ucloud_upathx_accelerator_id.placeholder\": \"请输入优刻得 UPathX 加速器实例 ID\",\r\n  \"workflow_node.deploy.form.ucloud_upathx_accelerator_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.ucloud.cn/upathx/accelerate\\\" target=\\\"_blank\\\">https://console.ucloud.cn/upathx/accelerate</a>\",\r\n  \"workflow_node.deploy.form.ucloud_upathx_listener_port.label\": \"优刻得 UPathX 加速器监听端口\",\r\n  \"workflow_node.deploy.form.ucloud_upathx_listener_port.placeholder\": \"请输入优刻得 UPathX 加速器监听端口\",\r\n  \"workflow_node.deploy.form.ucloud_upathx_listener_port.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.ucloud.cn/upathx/accelerate\\\" target=\\\"_blank\\\">https://console.ucloud.cn/upathx/accelerate</a>\",\r\n  \"workflow_node.deploy.form.ucloud_us3_region.label\": \"优刻得服务地域\",\r\n  \"workflow_node.deploy.form.ucloud_us3_region.placeholder\": \"优刻得 US3 服务地域（例如：cn-bj2）\",\r\n  \"workflow_node.deploy.form.ucloud_us3_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://docs.ucloud.cn/api/summary/regionlist\\\" target=\\\"_blank\\\">https://docs.ucloud.cn/api/summary/regionlist</a>\",\r\n  \"workflow_node.deploy.form.ucloud_us3_bucket.label\": \"优刻得 US3 存储桶名\",\r\n  \"workflow_node.deploy.form.ucloud_us3_bucket.placeholder\": \"请输入优刻得 US3 存储桶名\",\r\n  \"workflow_node.deploy.form.ucloud_us3_domain.label\": \"优刻得 US3 自定义域名\",\r\n  \"workflow_node.deploy.form.ucloud_us3_domain.placeholder\": \"请输入优刻得 US3 自定义域名\",\r\n  \"workflow_node.deploy.form.ucloud_uewaf_domain.label\": \"优刻得 UEWAF 防护域名\",\r\n  \"workflow_node.deploy.form.ucloud_uewaf_domain.placeholder\": \"请输入优刻得 UEWAF 防护域名\",\r\n  \"workflow_node.deploy.form.unicloud_webhost.guide\": \"由于 uniCloud 未公开相关 API，这里将使用网页模拟登录方式部署，但无法保证稳定性。如遇 uniCloud 接口变更，请到 GitHub 发起 Issue 告知。\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_space_provider.label\": \"uniCloud 服务空间提供商\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_space_provider.placeholder\": \"请选择 uniCloud 服务空间提供商\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_space_provider.option.aliyun.label\": \"阿里云\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_space_provider.option.tencent.label\": \"腾讯云\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_space_id.label\": \"uniCloud 服务空间 ID\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_space_id.placeholder\": \"请输入 uniCloud 服务空间 ID\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_space_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://doc.dcloud.net.cn/uniCloud/concepts/space.html\\\" target=\\\"_blank\\\">https://doc.dcloud.net.cn/uniCloud/concepts/space.html</a>\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_domain.label\": \"uniCloud 前端网页托管网站域名\",\r\n  \"workflow_node.deploy.form.unicloud_webhost_domain.placeholder\": \"请输入 uniCloud 前端网页托管网站域名\",\r\n  \"workflow_node.deploy.form.upyun_cdn.guide\": \"由于又拍云未公开相关 API，这里将使用网页模拟登录方式部署，但无法保证稳定性。如遇又拍云接口变更，请到 GitHub 发起 Issue 告知。\",\r\n  \"workflow_node.deploy.form.upyun_cdn_domain.label\": \"又拍云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.upyun_cdn_domain.placeholder\": \"请输入又拍云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.upyun_file.guide\": \"由于又拍云未公开相关 API，这里将使用网页模拟登录方式部署，但无法保证稳定性。如遇又拍云接口变更，请到 GitHub 发起 Issue 告知。\",\r\n  \"workflow_node.deploy.form.upyun_file_bucket.label\": \"又拍云云存储桶名\",\r\n  \"workflow_node.deploy.form.upyun_file_bucket.placeholder\": \"请输入又拍云云存储桶名\",\r\n  \"workflow_node.deploy.form.upyun_file_domain.label\": \"又拍云云存储自定义域名\",\r\n  \"workflow_node.deploy.form.upyun_file_domain.placeholder\": \"请输入又拍云云存储自定义域名\",\r\n  \"workflow_node.deploy.form.volcengine_alb_resource_type.option.loadbalancer.label\": \"部署到指定负载均衡器下的全部 HTTPS 监听\",\r\n  \"workflow_node.deploy.form.volcengine_alb_resource_type.option.listener.label\": \"部署到指定 HTTPS 监听器\",\r\n  \"workflow_node.deploy.form.volcengine_alb_region.label\": \"火山引擎服务地域\",\r\n  \"workflow_node.deploy.form.volcengine_alb_region.placeholder\": \"请输入火山引擎 ALB 服务地域（例如：cn-beijing）\",\r\n  \"workflow_node.deploy.form.volcengine_alb_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.volcengine.com/docs/6767/127501\\\" target=\\\"_blank\\\">https://www.volcengine.com/docs/6767/127501</a>\",\r\n  \"workflow_node.deploy.form.volcengine_alb_loadbalancer_id.label\": \"火山引擎 ALB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.volcengine_alb_loadbalancer_id.placeholder\": \"请输入火山引擎 ALB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.volcengine_alb_loadbalancer_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.volcengine.com/alb\\\" target=\\\"_blank\\\">https://console.volcengine.com/alb</a>\",\r\n  \"workflow_node.deploy.form.volcengine_alb_listener_id.label\": \"火山引擎 ALB 监听器 ID\",\r\n  \"workflow_node.deploy.form.volcengine_alb_listener_id.placeholder\": \"请输入火山引擎 ALB 监听器 ID\",\r\n  \"workflow_node.deploy.form.volcengine_alb_listener_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.volcengine.com/alb\\\" target=\\\"_blank\\\">https://console.volcengine.com/alb</a>\",\r\n  \"workflow_node.deploy.form.volcengine_alb_snidomain.label\": \"火山引擎 ALB 扩展域名（可选）\",\r\n  \"workflow_node.deploy.form.volcengine_alb_snidomain.placeholder\": \"请输入火山引擎 ALB 扩展域名\",\r\n  \"workflow_node.deploy.form.volcengine_alb_snidomain.help\": \"提示：不填写时，将替换监听器的默认证书；否则，将替换扩展域名证书。\",\r\n  \"workflow_node.deploy.form.volcengine_cdn_domain.label\": \"火山引擎 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.volcengine_cdn_domain.placeholder\": \"请输入火山引擎 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.volcengine_certcenter_region.label\": \"火山引擎服务地域\",\r\n  \"workflow_node.deploy.form.volcengine_certcenter_region.placeholder\": \"请输入火山引擎证书中心服务地域（例如：cn-beijing）\",\r\n  \"workflow_node.deploy.form.volcengine_clb_resource_type.option.loadbalancer.label\": \"部署到指定负载均衡器下的全部 HTTPS 监听\",\r\n  \"workflow_node.deploy.form.volcengine_clb_resource_type.option.listener.label\": \"部署到 HTTPS 监听器\",\r\n  \"workflow_node.deploy.form.volcengine_clb_region.label\": \"火山引擎服务地域\",\r\n  \"workflow_node.deploy.form.volcengine_clb_region.placeholder\": \"请输入火山引擎 CLB 服务地域（例如：cn-beijing）\",\r\n  \"workflow_node.deploy.form.volcengine_clb_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.volcengine.com/docs/6406/74892\\\" target=\\\"_blank\\\">https://www.volcengine.com/docs/6406/74892</a>\",\r\n  \"workflow_node.deploy.form.volcengine_clb_loadbalancer_id.label\": \"火山引擎 CLB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.volcengine_clb_loadbalancer_id.placeholder\": \"请输入火山引擎 CLB 负载均衡器 ID\",\r\n  \"workflow_node.deploy.form.volcengine_clb_loadbalancer_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.volcengine.com/clb/LoadBalancer\\\" target=\\\"_blank\\\">https://console.volcengine.com/clb/LoadBalancer</a>\",\r\n  \"workflow_node.deploy.form.volcengine_clb_listener_id.label\": \"火山引擎 CLB 监听器 ID\",\r\n  \"workflow_node.deploy.form.volcengine_clb_listener_id.placeholder\": \"请输入火山引擎 CLB 监听器 ID\",\r\n  \"workflow_node.deploy.form.volcengine_clb_listener_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.volcengine.com/clb/LoadBalancer\\\" target=\\\"_blank\\\">https://console.volcengine.com/clb/LoadBalancer</a>\",\r\n  \"workflow_node.deploy.form.volcengine_dcdn_domain.label\": \"火山引擎 DCDN 加速域名\",\r\n  \"workflow_node.deploy.form.volcengine_dcdn_domain.placeholder\": \"请输入火山引擎 DCDN 加速域名\",\r\n  \"workflow_node.deploy.form.volcengine_imagex_region.label\": \"火山引擎服务地域\",\r\n  \"workflow_node.deploy.form.volcengine_imagex_region.placeholder\": \"请输入火山引擎 ImageX 服务地域（例如：cn-north-1）\",\r\n  \"workflow_node.deploy.form.volcengine_imagex_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.volcengine.com/docs/508/23757\\\" target=\\\"_blank\\\">https://www.volcengine.com/docs/508/23757</a>\",\r\n  \"workflow_node.deploy.form.volcengine_imagex_service_id.label\": \"火山引擎 ImageX 服务 ID\",\r\n  \"workflow_node.deploy.form.volcengine_imagex_service_id.placeholder\": \"请输入火山引擎 ImageX 服务 ID\",\r\n  \"workflow_node.deploy.form.volcengine_imagex_service_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.volcengine.com/imagex\\\" target=\\\"_blank\\\">https://console.volcengine.com/imagex</a>\",\r\n  \"workflow_node.deploy.form.volcengine_imagex_domain.label\": \"火山引擎 ImageX 绑定域名\",\r\n  \"workflow_node.deploy.form.volcengine_imagex_domain.placeholder\": \"请输入火山引擎 ImageX 绑定域名\",\r\n  \"workflow_node.deploy.form.volcengine_live_domain.label\": \"火山引擎视频直播流域名\",\r\n  \"workflow_node.deploy.form.volcengine_live_domain.placeholder\": \"请输入火山引擎视频直播流域名\",\r\n  \"workflow_node.deploy.form.volcengine_tos_region.label\": \"火山引擎服务地域\",\r\n  \"workflow_node.deploy.form.volcengine_tos_region.placeholder\": \"请输入火山引擎 TOS 服务地域（例如：cn-beijing）\",\r\n  \"workflow_node.deploy.form.volcengine_tos_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.volcengine.com/docs/6349/107356\\\" target=\\\"_blank\\\">https://www.volcengine.com/docs/6349/107356</a>\",\r\n  \"workflow_node.deploy.form.volcengine_tos_bucket.label\": \"火山引擎 TOS 存储桶名\",\r\n  \"workflow_node.deploy.form.volcengine_tos_bucket.placeholder\": \"请输入火山引擎 TOS 存储桶名\",\r\n  \"workflow_node.deploy.form.volcengine_tos_domain.label\": \"火山引擎 TOS 自定义域名\",\r\n  \"workflow_node.deploy.form.volcengine_tos_domain.placeholder\": \"请输入火山引擎 TOS 自定义域名\",\r\n  \"workflow_node.deploy.form.volcengine_vod_space_name.label\": \"火山引擎 VOD 空间名称\",\r\n  \"workflow_node.deploy.form.volcengine_vod_space_name.placeholder\": \"请输入火山引擎 VOD 空间名称\",\r\n  \"workflow_node.deploy.form.volcengine_vod_space_name.tooltip\": \"这是什么？请参阅 <a href=\\\"https://console.volcengine.com/vod/overview\\\" target=\\\"_blank\\\">https://console.volcengine.com/vod/overview</a>\",\r\n  \"workflow_node.deploy.form.volcengine_vod_domain_type.label\": \"火山引擎 VOD 域名类型\",\r\n  \"workflow_node.deploy.form.volcengine_vod_domain_type.placeholder\": \"请选择火山引擎 VOD 域名类型\",\r\n  \"workflow_node.deploy.form.volcengine_vod_domain_type.option.play.label\": \"点播加速域名\",\r\n  \"workflow_node.deploy.form.volcengine_vod_domain_type.option.image.label\": \"封面加速域名\",\r\n  \"workflow_node.deploy.form.volcengine_vod_domain.label\": \"火山引擎 VOD 加速域名\",\r\n  \"workflow_node.deploy.form.volcengine_vod_domain.placeholder\": \"请输入火山引擎 VOD 加速域名\",\r\n  \"workflow_node.deploy.form.volcengine_waf_region.label\": \"火山引擎服务地域\",\r\n  \"workflow_node.deploy.form.volcengine_waf_region.placeholder\": \"请输入火山引擎 WAF 服务地域（例如：cn-beijing）\",\r\n  \"workflow_node.deploy.form.volcengine_waf_region.tooltip\": \"这是什么？请参阅 <a href=\\\"https://www.volcengine.com/docs/6511/1594024\\\" target=\\\"_blank\\\">https://www.volcengine.com/docs/6511/1594024</a>\",\r\n  \"workflow_node.deploy.form.volcengine_waf_access_mode.label\": \"火山引擎 WAF 接入模式\",\r\n  \"workflow_node.deploy.form.volcengine_waf_access_mode.placeholder\": \"请选择火山引擎 WAF 接入模式\",\r\n  \"workflow_node.deploy.form.volcengine_waf_access_mode.option.cname.label\": \"CNAME 接入\",\r\n  \"workflow_node.deploy.form.volcengine_waf_domain.label\": \"火山引擎 WAF 防护域名\",\r\n  \"workflow_node.deploy.form.volcengine_waf_domain.placeholder\": \"请输入火山引擎 WAF 防护域名\",\r\n  \"workflow_node.deploy.form.wangsu_cdn_domains.label\": \"网宿云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.wangsu_cdn_domains.placeholder\": \"请输入网宿云 CDN 加速域名（多个值请用半角分号隔开）\",\r\n  \"workflow_node.deploy.form.wangsu_cdn_domains.help\": \"提示：支持多个域名，以半角分号隔开。\",\r\n  \"workflow_node.deploy.form.wangsu_cdn_domains.multiple_input_modal.title\": \"修改网宿云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.wangsu_cdn_domains.multiple_input_modal.placeholder\": \"请输入网宿云 CDN 加速域名\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_environment.label\": \"网宿云环境\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder\": \"请选择网宿云环境\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_environment.option.production.label\": \"生产环境\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_environment.option.staging.label\": \"演练环境\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_domain.label\": \"网宿云 CDN Pro 加速域名\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder\": \"请输入网宿云 CDN Pro 加速域名\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.label\": \"网宿云 CDN Pro 原证书 ID（可选）\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.placeholder\": \"请输入网宿云 CDN Pro 原证书 ID\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.help\": \"提示：不填写时，将上传新证书；否则，将替换原证书。\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cdnpro.console.wangsu.com/v2/index/#/certificate\\\" target=\\\"_blank\\\">https://cdnpro.console.wangsu.com/v2/index/#/certificate</a>\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.label\": \"网宿云 CDN Pro 部署任务 Webhook ID（可选）\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.placeholder\": \"请输入网宿云 CDN Pro 部署任务 Webhook ID\",\r\n  \"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cdnpro.console.wangsu.com/v2/index/#/certificate\\\" target=\\\"_blank\\\">https://cdnpro.console.wangsu.com/v2/index/#/certificate</a>\",\r\n  \"workflow_node.deploy.form.wangsu_certificate_id.label\": \"网宿云证书 ID（可选）\",\r\n  \"workflow_node.deploy.form.wangsu_certificate_id.placeholder\": \"请输入网宿云证书 ID\",\r\n  \"workflow_node.deploy.form.wangsu_certificate_id.help\": \"提示：不填写时，将上传新证书；否则，将替换原证书。\",\r\n  \"workflow_node.deploy.form.wangsu_certificate_id.tooltip\": \"这是什么？请参阅 <a href=\\\"https://cdn.console.wangsu.com/v2/index#/certificate/list?code=cert_mylist&parentCode=cert_ssl&productCode=certificatemanagement\\\" target=\\\"_blank\\\">https://cdn.console.wangsu.com/v2/index#/certificate/list</a>\",\r\n  \"workflow_node.deploy.form.webhook_data.label\": \"Webhook 回调数据（可选）\",\r\n  \"workflow_node.deploy.form.webhook_data.placeholder\": \"请输入 Webhook 回调数据以覆盖默认值\",\r\n  \"workflow_node.deploy.form.webhook_data.help\": \"提示：不填写时，将使用所选部署目标授权的默认 Webhook 回调数据。\",\r\n  \"workflow_node.deploy.form.webhook_data.vartips\": \"支持的变量：<br><ol style=\\\"list-style: disc;\\\"><li><strong>${CERTIMATE_DEPLOYER_COMMONNAME}</strong>：<br>证书的主域名或 IP。</li><li><strong>${CERTIMATE_DEPLOYER_SUBJECTALTNAMES}</strong>：<br>证书的多域名或 IP，以半角分号隔开。</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE}</strong>：<br>证书文件 PEM 格式内容。</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE_SERVER}</strong>：<br>证书文件（仅含服务器证书）PEM 格式内容。</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE_INTERMEDIA}</strong>：<br>证书文件（仅含中间证书）PEM 格式内容。</li><li><strong>${CERTIMATE_DEPLOYER_PRIVATEKEY}</strong>：<br>私钥文件 PEM 格式内容。</li></ol>\",\r\n  \"workflow_node.deploy.form.webhook_timeout.label\": \"Webhook 超时时间（可选）\",\r\n  \"workflow_node.deploy.form.webhook_timeout.placeholder\": \"请输入 Webhook 超时时间\",\r\n  \"workflow_node.deploy.form.webhook_timeout.unit\": \"秒\",\r\n  \"workflow_node.deploy.form.skip_on_last_succeeded.label\": \"重复部署\",\r\n  \"workflow_node.deploy.form.skip_on_last_succeeded.prefix\": \"当上次部署相同证书成功后，再次运行工作流时\",\r\n  \"workflow_node.deploy.form.skip_on_last_succeeded.suffix\": \"此部署节点。\",\r\n  \"workflow_node.deploy.form.skip_on_last_succeeded.switch.on\": \"跳过\",\r\n  \"workflow_node.deploy.form.skip_on_last_succeeded.switch.off\": \"不跳过\",\r\n\r\n  \"workflow_node.notify.label\": \"推送通知\",\r\n  \"workflow_node.notify.default_name\": \"通知\",\r\n  \"workflow_node.notify.form_anchor.parameters.tab\": \"参数设置\",\r\n  \"workflow_node.notify.form_anchor.channel.tab\": \"渠道设置\",\r\n  \"workflow_node.notify.form_anchor.channel.title\": \"渠道设置\",\r\n  \"workflow_node.notify.form_anchor.strategy.tab\": \"执行策略\",\r\n  \"workflow_node.notify.form_anchor.strategy.title\": \"执行策略\",\r\n  \"workflow_node.notify.form.subject.label\": \"通知主题\",\r\n  \"workflow_node.notify.form.subject.placeholder\": \"请输入通知主题\",\r\n  \"workflow_node.notify.form.message.label\": \"通知内容\",\r\n  \"workflow_node.notify.form.message.placeholder\": \"请输入通知内容\",\r\n  \"workflow_node.notify.form.template.guide\": \"<details><summary>通知主题或内容中使用「Mustache」语法（即双大括号）包裹、并以「$」符号开头的文本会被视为模板插值，将在推送时被替换为实际值。</summary><br>支持的模板插值：<ol style=\\\"list-style: disc;\\\"><li>工作流相关：<ol style=\\\"margin: 0; list-style: '-';\\\"><li><em>workflow.id</em>：工作流 ID。</li><li><em>workflow.name</em>：工作流名称。</li><li><em>run.id</em>：运行 ID。</li></ol></li><li>异常相关：<br><i>（如果在此之前有多个执行失败的节点，始终表示最近的一个。）</i><ol style=\\\"margin: 0; list-style: '-';\\\"><li><em>error.nodeId</em>：执行失败时的节点 ID。</li><li><em>error.nodeName</em>：执行失败时的节点名称。</li><li><em>error.message</em>：执行失败时的错误信息。</li></ol></li><li>证书相关：<br><i>（如果在此之前有多个输出证书的节点，始终表示最近的一个。）</i><ol style=\\\"margin: 0; list-style: '-';\\\"><li><em>certificate.commonName</em>：证书主域名或 IP。</li><li><em>certificate.subjectAltNames</em>：证书多域名或 IP，以半角分号隔开。</li><li><em>certificate.notBefore</em>：证书生效时间，以 RFC3339 格式化。</li><li><em>certificate.notAfter</em>：证书过期时间，以 RFC3339 格式化。</li><li><em>certificate.hoursLeft</em>：证书剩余小时数。</li><li><em>certificate.daysLeft</em>：证书剩余天数。</li><li><em>certificate.validity</em>：证书是否有效。</li></ol></li><li>其他：<ol style=\\\"margin: 0; list-style: '-';\\\"><li><em>now</em>：服务器当前时间，以 RFC3339 格式化。</li></ol></li></ol><br>示例：<br><em>Your workflow {{ $workflow.name }} has failed on node {{ $error.nodeName }} at {{ $now }}.</em></details>\",\r\n  \"workflow_node.notify.form.provider.label\": \"通知渠道\",\r\n  \"workflow_node.notify.form.provider.placeholder\": \"请选择通知渠道\",\r\n  \"workflow_node.notify.form.provider.search.placeholder\": \"搜索通知渠道……\",\r\n  \"workflow_node.notify.form.provider_access.label\": \"通知渠道授权\",\r\n  \"workflow_node.notify.form.provider_access.placeholder\": \"请选择通知渠道授权\",\r\n  \"workflow_node.notify.form.provider_access.button\": \"新建\",\r\n  \"workflow_node.notify.form.params_config.label\": \"参数设置\",\r\n  \"workflow_node.notify.form.discordbot_channel_id.label\": \"Discord 频道 ID（可选）\",\r\n  \"workflow_node.notify.form.discordbot_channel_id.placeholder\": \"请输入 Discord 频道 ID\",\r\n  \"workflow_node.notify.form.discordbot_channel_id.help\": \"提示：不填写时，将使用所选通知渠道授权的默认频道 ID。\",\r\n  \"workflow_node.notify.form.email_format.label\": \"消息格式（可选）\",\r\n  \"workflow_node.notify.form.email_format.placeholder\": \"请选择消息格式\",\r\n  \"workflow_node.notify.form.email_format.option.plain.label\": \"纯文本\",\r\n  \"workflow_node.notify.form.email_format.option.html.label\": \"HTML\",\r\n  \"workflow_node.notify.form.email_receiver_address.label\": \"收件人邮箱（可选）\",\r\n  \"workflow_node.notify.form.email_receiver_address.placeholder\": \"请输入收件人邮箱以覆盖默认值\",\r\n  \"workflow_node.notify.form.email_receiver_address.help\": \"提示：不填写时，将使用所选通知渠道授权的默认收件人邮箱。\",\r\n  \"workflow_node.notify.form.mattermost_channel_id.label\": \"Mattermost 频道 ID（可选）\",\r\n  \"workflow_node.notify.form.mattermost_channel_id.placeholder\": \"请输入 Mattermost 频道 ID\",\r\n  \"workflow_node.notify.form.mattermost_channel_id.help\": \"提示：不填写时，将使用所选通知渠道授权的默认频道 ID。\",\r\n  \"workflow_node.notify.form.slackbot_channel_id.label\": \"Slack 频道 ID（可选）\",\r\n  \"workflow_node.notify.form.slackbot_channel_id.placeholder\": \"请输入 Slack 频道 ID\",\r\n  \"workflow_node.notify.form.slackbot_channel_id.help\": \"提示：不填写时，将使用所选通知渠道授权的默认频道 ID。\",\r\n  \"workflow_node.notify.form.telegrambot_chat_id.label\": \"Telegram 会话 ID（可选）\",\r\n  \"workflow_node.notify.form.telegrambot_chat_id.placeholder\": \"请输入 Telegram 会话 ID\",\r\n  \"workflow_node.notify.form.telegrambot_chat_id.help\": \"提示：不填写时，将使用所选通知渠道授权的默认会话 ID。\",\r\n  \"workflow_node.notify.form.webhook_data.label\": \"Webhook 回调数据（可选）\",\r\n  \"workflow_node.notify.form.webhook_data.placeholder\": \"请输入 Webhook 回调数据以覆盖默认值\",\r\n  \"workflow_node.notify.form.webhook_data.help\": \"提示：不填写时，将使用所选通知渠道授权的默认 Webhook 回调数据。\",\r\n  \"workflow_node.notify.form.webhook_data.vartips\": \"支持的变量：<br><ol style=\\\"list-style: disc;\\\"><li><strong>${CERTIMATE_NOTIFIER_SUBJECT}</strong>：<br>通知主题。</li><li><strong>${CERTIMATE_NOTIFIER_MESSAGE}</strong>：<br>通知内容。</ol>\",\r\n  \"workflow_node.notify.form.webhook_timeout.label\": \"Webhook 超时时间（可选）\",\r\n  \"workflow_node.notify.form.webhook_timeout.placeholder\": \"请输入 Webhook 超时时间\",\r\n  \"workflow_node.notify.form.webhook_timeout.unit\": \"秒\",\r\n  \"workflow_node.notify.form.skip_on_all_prev_skipped.label\": \"静默行为\",\r\n  \"workflow_node.notify.form.skip_on_all_prev_skipped.prefix\": \"当前序申请、上传、部署等节点均已跳过执行时，\",\r\n  \"workflow_node.notify.form.skip_on_all_prev_skipped.suffix\": \"此通知节点。\",\r\n  \"workflow_node.notify.form.skip_on_all_prev_skipped.switch.on\": \"跳过\",\r\n  \"workflow_node.notify.form.skip_on_all_prev_skipped.switch.off\": \"不跳过\",\r\n\r\n  \"workflow_node.delay.label\": \"延迟等待\",\r\n  \"workflow_node.delay.default_name\": \"延迟\",\r\n  \"workflow_node.delay.form_anchor.parameters.tab\": \"参数设置\",\r\n  \"workflow_node.delay.form.wait.label\": \"等待时间\",\r\n  \"workflow_node.delay.form.wait.placeholder\": \"请输入等待时间\",\r\n  \"workflow_node.delay.form.wait.unit\": \"秒\",\r\n\r\n  \"workflow_node.condition.label\": \"并行/条件分支\",\r\n  \"workflow_node.condition.default_name\": \"并行\",\r\n  \"workflow_node.condition.default_name.template_certtest_on_expiring_soon\": \"若网站证书即将过期…\",\r\n  \"workflow_node.condition.default_name.template_certtest_on_expired\": \"若网站证书已过期…\",\r\n\r\n  \"workflow_node.branch_block.label\": \"分支\",\r\n  \"workflow_node.branch_block.default_name\": \"分支\",\r\n  \"workflow_node.branch_block.state.no\": \"无条件进入\",\r\n  \"workflow_node.branch_block.state.or\": \"满足任一条件时进入\",\r\n  \"workflow_node.branch_block.state.and\": \"满足所有条件时进入\",\r\n  \"workflow_node.branch_block.form_anchor.parameters.tab\": \"参数设置\",\r\n  \"workflow_node.branch_block.form.expression.label\": \"分支进入条件\",\r\n  \"workflow_node.branch_block.form.expression.errmsg.invalid\": \"请输入有效的条件\",\r\n  \"workflow_node.branch_block.form.expression.logical_operator.errmsg\": \"请选择条件组合方式\",\r\n  \"workflow_node.branch_block.form.expression.logical_operator.option.and.label\": \"满足以下所有条件 (AND)\",\r\n  \"workflow_node.branch_block.form.expression.logical_operator.option.or.label\": \"满足以下任一条件 (OR)\",\r\n  \"workflow_node.branch_block.form.expression.variable.placeholder\": \"请选择\",\r\n  \"workflow_node.branch_block.form.expression.variable.errmsg\": \"请选择变量\",\r\n  \"workflow_node.branch_block.form.expression.operator.placeholder\": \"请选择\",\r\n  \"workflow_node.branch_block.form.expression.operator.errmsg\": \"请选择运算符\",\r\n  \"workflow_node.branch_block.form.expression.operator.option.eq.label\": \"等于\",\r\n  \"workflow_node.branch_block.form.expression.operator.option.eq.alias_is_label\": \"为\",\r\n  \"workflow_node.branch_block.form.expression.operator.option.neq.label\": \"不等于\",\r\n  \"workflow_node.branch_block.form.expression.operator.option.neq.alias_not_label\": \"不为\",\r\n  \"workflow_node.branch_block.form.expression.operator.option.gt.label\": \"大于\",\r\n  \"workflow_node.branch_block.form.expression.operator.option.gte.label\": \"大于等于\",\r\n  \"workflow_node.branch_block.form.expression.operator.option.lt.label\": \"小于\",\r\n  \"workflow_node.branch_block.form.expression.operator.option.lte.label\": \"小于等于\",\r\n  \"workflow_node.branch_block.form.expression.value.placeholder\": \"请输入\",\r\n  \"workflow_node.branch_block.form.expression.value.errmsg\": \"请输入值\",\r\n  \"workflow_node.branch_block.form.expression.value.option.true.label\": \"真\",\r\n  \"workflow_node.branch_block.form.expression.value.option.false.label\": \"假\",\r\n  \"workflow_node.branch_block.form.expression.add_condition.button\": \"添加条件\",\r\n\r\n  \"workflow_node.try_catch.label\": \"执行结果分支\",\r\n  \"workflow_node.try_catch.default_name\": \"尝试执行…\",\r\n\r\n  \"workflow_node.catch_block.label\": \"执行失败分支\",\r\n  \"workflow_node.catch_block.default_name\": \"若执行失败…\",\r\n\r\n  \"workflow_node.end.label\": \"结束\",\r\n  \"workflow_node.end.default_name\": \"结束\"\r\n}\r\n"
  },
  {
    "path": "ui/src/i18n/locales/zh/nls.workflow.runs.json",
    "content": "{\n  \"workflow_run.action.view.menu\": \"查看详情\",\n  \"workflow_run.action.cancel.menu\": \"取消运行\",\n  \"workflow_run.action.cancel.modal.title\": \"取消运行\",\n  \"workflow_run.action.cancel.modal.content\": \"确定要取消此运行吗？取消后仅会中止流程，但不会回滚已执行的节点。\",\n  \"workflow_run.action.delete.menu\": \"删除运行\",\n  \"workflow_run.action.delete.modal.title\": \"删除运行「{{name}}」\",\n  \"workflow_run.action.delete.modal.content\": \"确定要删除该工作流运行历史吗？删除后仅会清除日志历史，但不会影响签发的证书。<br>注意此操作不可撤销，请谨慎操作。\",\n  \"workflow_run.action.batch_delete.modal.title\": \"删除运行历史\",\n  \"workflow_run.action.batch_delete.modal.content\": \"确定要删除这 {{count}} 个被选中的工作流运行历史吗？删除后仅会清除日志历史，但不会影响签发的证书。<br>注意此操作不可撤销，请谨慎操作。\",\n\n  \"workflow_run.deletion.alert\": \"运行历史中包含工作流各节点的运行结果，删除后可能导致因找不到前次运行结果而触发重新申请或部署证书。如无必要请勿提前删除，建议保留至少 180 天。\",\n  \"workflow_run.cancellation.alert\": \"如遇进程意外中止、服务器超时等原因，你可以手动取消长时间挂起的运行，避免阻塞后续运行。\",\n\n  \"workflow_run.nodata.title\": \"暂无工作流运行\",\n  \"workflow_run.nodata.description\": \"当前未找到任何运行历史。请先运行此工作流。\",\n\n  \"workflow_run.props.workflow\": \"工作流\",\n  \"workflow_run.props.status\": \"状态\",\n  \"workflow_run.props.status.pending\": \"等待运行\",\n  \"workflow_run.props.status.processing\": \"运行中\",\n  \"workflow_run.props.status.succeeded\": \"已成功\",\n  \"workflow_run.props.status.failed\": \"已失败\",\n  \"workflow_run.props.status.canceled\": \"已取消\",\n  \"workflow_run.props.trigger\": \"触发方式\",\n  \"workflow_run.props.trigger.scheduled\": \"定时\",\n  \"workflow_run.props.trigger.manual\": \"手动\",\n  \"workflow_run.props.started_at\": \"开始时间\",\n  \"workflow_run.props.ended_at\": \"完成时间\",\n  \"workflow_run.props.artifacts\": \"输出产物\",\n\n  \"workflow_run.base.description\": \"{{trigger}}触发于 {{startedAt}}\",\n  \"workflow_run.base.description_with_time_cost\": \"{{trigger}}触发于 {{startedAt}}，总计用时 {{timeCost}}。\",\n  \"workflow_run.base.trigger.scheduled\": \"定时\",\n  \"workflow_run.base.trigger.manual\": \"手动\",\n\n  \"workflow_run.process\": \"流程\",\n  \"workflow_run.process.menu.export\": \"导出\",\n\n  \"workflow_run.logs\": \"日志\",\n  \"workflow_run.logs.menu.show_timestamps\": \"显示日期时间\",\n  \"workflow_run.logs.menu.show_whitespaces\": \"显示转义换行符\",\n  \"workflow_run.logs.menu.download_logs\": \"下载日志\",\n\n  \"workflow_run.artifacts\": \"输出产物\",\n  \"workflow_run_artifact.props.type\": \"类型\",\n  \"workflow_run_artifact.props.type.certificate\": \"证书\",\n  \"workflow_run_artifact.props.name\": \"名称\"\n}\n"
  },
  {
    "path": "ui/src/i18n/locales/zh/nls.workflow.vars.json",
    "content": "{\n  \"workflow.variables.type.certificate.label\": \"证书\",\n\n  \"workflow.variables.selector.hours_left.label\": \"剩余小时数\",\n  \"workflow.variables.selector.days_left.label\": \"剩余天数\",\n  \"workflow.variables.selector.validity.label\": \"有效性\"\n}\n"
  },
  {
    "path": "ui/src/index.css",
    "content": ":root {\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n  font-weight: 400;\n  font-synthesis: none;\n  line-height: 1.5;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\nbody {\n  margin: 0;\n  padding: 0;\n  color: var(--color-foreground);\n  background: var(--color-background);\n  min-width: 360px;\n  min-height: 540px;\n  min-height: 100vh;\n  min-height: calc(min(540px, 100vh));\n}\n"
  },
  {
    "path": "ui/src/main.tsx",
    "content": "import { StrictMode } from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport dayjs from \"dayjs\";\nimport dayjsUtc from \"dayjs/plugin/utc\";\n\nimport App from \"./App\";\nimport \"./i18n\";\nimport \"./index.css\";\nimport \"./global.css\";\n\ndayjs.extend(dayjsUtc);\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);\n"
  },
  {
    "path": "ui/src/pages/AuthLayout.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { Navigate, Outlet } from \"react-router-dom\";\nimport { Alert, Layout } from \"antd\";\n\nimport Show from \"@/components/Show\";\nimport { getAuthStore } from \"@/repository/admin\";\nimport { isBrowserHappy } from \"@/utils/browser\";\n\nconst AuthLayout = () => {\n  const { t } = useTranslation();\n\n  const auth = getAuthStore();\n  if (auth.isValid && auth.isSuperuser) {\n    return <Navigate to=\"/\" />;\n  }\n\n  return (\n    <Layout className=\"h-screen\">\n      <Show when={!isBrowserHappy()}>\n        <Alert banner closable showIcon title={t(\"common.text.happy_browser\")} type=\"warning\" />\n      </Show>\n\n      <div className=\"relative\">\n        <Outlet />\n      </div>\n    </Layout>\n  );\n};\n\nexport default AuthLayout;\n"
  },
  {
    "path": "ui/src/pages/ConsoleLayout.tsx",
    "content": "import { memo, useCallback, useEffect, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Navigate, Outlet, useLocation, useNavigate } from \"react-router-dom\";\nimport {\n  IconBrandGithub,\n  IconCertificate,\n  IconCodeDots,\n  IconFingerprint,\n  IconHelpCircle,\n  IconHierarchy3,\n  IconHome,\n  IconLayoutSidebarLeftCollapse,\n  IconLayoutSidebarRightCollapse,\n  IconMenu2,\n  IconPower,\n  IconSettings,\n} from \"@tabler/icons-react\";\nimport { Alert, Button, Drawer, Layout, Menu, type MenuProps, theme } from \"antd\";\n\nimport AppLocale from \"@/components/AppLocale\";\nimport AppTheme from \"@/components/AppTheme\";\nimport AppVersion from \"@/components/AppVersion\";\nimport Show from \"@/components/Show\";\nimport { APP_DOCUMENT_URL, APP_REPO_URL } from \"@/domain/app\";\nimport { useTriggerElement } from \"@/hooks\";\nimport { getAuthStore } from \"@/repository/admin\";\nimport { isBrowserHappy } from \"@/utils/browser\";\n\nconst ConsoleLayout = () => {\n  const navigate = useNavigate();\n\n  const { t } = useTranslation();\n\n  const { token: themeToken } = theme.useToken();\n\n  const [siderCollapsed, setSiderCollapsed] = useState(false);\n\n  const handleLogoutClick = () => {\n    auth.clear();\n    navigate(\"/login\");\n  };\n\n  const handleDocumentClick = () => {\n    window.open(APP_DOCUMENT_URL, \"_blank\");\n  };\n\n  const handleGitHubClick = () => {\n    window.open(APP_REPO_URL, \"_blank\");\n  };\n\n  const auth = getAuthStore();\n  if (!auth.isValid || !auth.isSuperuser) {\n    return <Navigate to=\"/login\" />;\n  }\n\n  return (\n    <Layout className=\"h-screen bg-background text-foreground\">\n      <Show when={!isBrowserHappy()}>\n        <Alert banner closable showIcon title={t(\"common.text.happy_browser\")} type=\"warning\" />\n      </Show>\n\n      <Layout className=\"h-screen\" hasSider>\n        <Layout.Sider\n          className=\"group/sider z-20 h-full border-r bg-background max-md:static max-md:hidden\"\n          style={{ borderColor: themeToken.colorBorderSecondary }}\n          theme=\"light\"\n          width={siderCollapsed ? 81 : 256}\n        >\n          <div className=\"flex size-full flex-col items-center justify-between overflow-hidden select-none\">\n            <div className=\"w-full px-2\">\n              <SiderMenu collapsed={siderCollapsed} />\n            </div>\n            <div className=\"w-full px-2 pb-2\">\n              <Menu\n                style={{ background: \"transparent\", borderInlineEnd: \"none\" }}\n                inlineCollapsed={siderCollapsed}\n                items={[\n                  {\n                    key: \"document\",\n                    icon: (\n                      <span className=\"anticon scale-125\" role=\"img\">\n                        <IconHelpCircle size=\"1em\" />\n                      </span>\n                    ),\n                    label: t(\"common.menu.gethelp\"),\n                    onClick: handleDocumentClick,\n                  },\n                  {\n                    key: \"logout\",\n                    danger: true,\n                    icon: (\n                      <span className=\"anticon scale-125\" role=\"img\">\n                        <IconPower size=\"1em\" />\n                      </span>\n                    ),\n                    label: t(\"common.menu.logout\"),\n                    onClick: handleLogoutClick,\n                  },\n                ]}\n                mode=\"vertical\"\n                selectable={false}\n              />\n            </div>\n          </div>\n          <div className=\"absolute top-1/2 right-0 translate-x-1/2 -translate-y-1/2 opacity-0 transition-opacity group-hover/sider:opacity-100\">\n            <Button\n              className=\"bg-background shadow-sm\"\n              icon={\n                siderCollapsed ? (\n                  <IconLayoutSidebarRightCollapse size=\"1.5em\" stroke=\"1.25\" color=\"#999\" />\n                ) : (\n                  <IconLayoutSidebarLeftCollapse size=\"1.5em\" stroke=\"1.25\" color=\"#999\" />\n                )\n              }\n              shape=\"circle\"\n              type=\"text\"\n              onClick={() => setSiderCollapsed(!siderCollapsed)}\n            />\n          </div>\n        </Layout.Sider>\n\n        <Layout className=\"flex flex-col overflow-hidden\">\n          <Layout.Header\n            className=\"relative border-b shadow-sm md:hidden\"\n            style={{\n              padding: 0,\n              borderBottomColor: themeToken.colorBorderSecondary,\n            }}\n          >\n            <div className=\"absolute inset-0 z-0\">\n              <div\n                className=\"h-full w-full\"\n                style={{\n                  backgroundImage:\n                    \"linear-gradient(rgba(255, 255, 255, 0.063) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.063) 1px, transparent 1px)\",\n                  backgroundSize: \"20px 20px\",\n                }}\n              >\n                <div className=\"h-full w-full backdrop-blur-[1px]\"></div>\n              </div>\n            </div>\n            <div className=\"flex size-full items-center justify-between overflow-hidden px-4\">\n              <div className=\"flex items-center gap-4\">\n                <SiderMenuDrawer trigger={<Button icon={<IconMenu2 size=\"1.25em\" stroke=\"1.25\" />} />} />\n              </div>\n              <div className=\"flex size-full grow items-center justify-end gap-4 overflow-hidden\">\n                <AppTheme.Dropdown>\n                  <Button icon={<AppTheme.Icon size=\"1.25em\" stroke=\"1.25\" />} />\n                </AppTheme.Dropdown>\n                <AppLocale.Dropdown>\n                  <Button icon={<AppLocale.Icon size=\"1.25em\" stroke=\"1.25\" />} />\n                </AppLocale.Dropdown>\n                <AppVersion.Badge>\n                  <Button icon={<IconBrandGithub size=\"1.25em\" stroke=\"1.25\" />} onClick={handleGitHubClick} />\n                </AppVersion.Badge>\n                <Button danger icon={<IconPower size=\"1.25em\" stroke=\"1.25\" />} onClick={handleLogoutClick} />\n              </div>\n            </div>\n          </Layout.Header>\n\n          <Layout.Content className=\"relative flex-1 overflow-x-hidden overflow-y-auto\">\n            <Outlet />\n          </Layout.Content>\n        </Layout>\n      </Layout>\n    </Layout>\n  );\n};\n\nconst SiderMenu = memo(({ collapsed, onSelect }: { collapsed?: boolean; onSelect?: (key: string) => void }) => {\n  const location = useLocation();\n  const navigate = useNavigate();\n\n  const { t } = useTranslation();\n\n  const MENU_KEY_HOME = \"/\";\n  const MENU_KEY_WORKFLOWS = \"/workflows\";\n  const MENU_KEY_CERTIFICATES = \"/certificates\";\n  const MENU_KEY_ACCESSES = \"/accesses\";\n  const MENU_KEY_PRESETS = \"/presets\";\n  const MENU_KEY_SETTINGS = \"/settings\";\n  const menuItems: Required<MenuProps>[\"items\"] = (\n    [\n      [MENU_KEY_HOME, \"dashboard.page.title\", <IconHome size=\"1em\" />],\n      [MENU_KEY_WORKFLOWS, \"workflow.page.title\", <IconHierarchy3 size=\"1em\" />],\n      [MENU_KEY_CERTIFICATES, \"certificate.page.title\", <IconCertificate size=\"1em\" />],\n      [MENU_KEY_ACCESSES, \"access.page.title\", <IconFingerprint size=\"1em\" />],\n      [MENU_KEY_PRESETS, \"preset.page.title\", <IconCodeDots size=\"1em\" />],\n      [MENU_KEY_SETTINGS, \"settings.page.title\", <IconSettings size=\"1em\" />],\n    ] satisfies Array<[string, string, React.ReactNode]>\n  ).map(([key, label, icon]) => {\n    return {\n      key: key,\n      icon: (\n        <span className=\"anticon scale-125\" role=\"img\">\n          {icon}\n        </span>\n      ),\n      label: t(label),\n      onClick: () => {\n        navigate(key);\n        onSelect?.(key);\n      },\n    };\n  });\n  const [menuSelectedKey, setMenuSelectedKey] = useState<string>();\n\n  const getActiveMenuItem = () => {\n    const item =\n      menuItems.find((item) => item!.key === location.pathname) ??\n      menuItems.find((item) => item!.key !== MENU_KEY_HOME && location.pathname.startsWith(item!.key as string));\n    return item;\n  };\n\n  useEffect(() => {\n    const item = getActiveMenuItem();\n    if (item) {\n      setMenuSelectedKey(item.key as string);\n    } else {\n      setMenuSelectedKey(void 0);\n    }\n  }, [location.pathname]);\n\n  useEffect(() => {\n    if (menuSelectedKey && menuSelectedKey !== getActiveMenuItem()?.key) {\n      navigate(menuSelectedKey);\n    }\n  }, [menuSelectedKey]);\n\n  return (\n    <>\n      <div className=\"h-[64px] w-full overflow-hidden px-4 py-2 max-md:py-0\">\n        <div className=\"flex size-full items-center justify-around gap-2\">\n          <img src=\"/logo.svg\" className=\"size-[36px]\" />\n          <Show when={!collapsed}>\n            <span className=\"w-[81px] truncate text-base leading-[64px] font-semibold\">Certimate</span>\n            <AppVersion.LinkButton className=\"text-xs\" />\n          </Show>\n        </div>\n      </div>\n      <div className=\"w-full grow overflow-x-hidden overflow-y-auto\">\n        <Menu\n          style={{ background: \"transparent\", borderInlineEnd: \"none\" }}\n          inlineCollapsed={collapsed}\n          items={menuItems}\n          mode=\"vertical\"\n          selectedKeys={menuSelectedKey ? [menuSelectedKey] : []}\n          onSelect={({ key }) => {\n            setMenuSelectedKey(key);\n          }}\n        />\n      </div>\n    </>\n  );\n});\n\nconst SiderMenuDrawer = memo(({ trigger }: { trigger: React.ReactNode }) => {\n  const { token: themeToken } = theme.useToken();\n\n  const [siderOpen, setSiderOpen] = useState(false);\n\n  const triggerEl = useTriggerElement(trigger, { onClick: () => setSiderOpen(true) });\n\n  const handleMenuSelect = useCallback(() => {\n    setSiderOpen(false);\n  }, []);\n\n  return (\n    <>\n      {triggerEl}\n\n      <Drawer\n        closable={false}\n        destroyOnHidden\n        open={siderOpen}\n        placement=\"left\"\n        styles={{\n          section: { paddingTop: themeToken.paddingSM, paddingBottom: themeToken.paddingSM },\n          body: { padding: 0 },\n        }}\n        onClose={() => setSiderOpen(false)}\n      >\n        <SiderMenu onSelect={handleMenuSelect} />\n      </Drawer>\n    </>\n  );\n});\n\nexport default ConsoleLayout;\n"
  },
  {
    "path": "ui/src/pages/ErrorLayout.tsx",
    "content": "import { Outlet } from \"react-router-dom\";\nimport { Layout } from \"antd\";\n\nconst ErrorLayout = ({ children }: { children?: React.ReactNode }) => {\n  return (\n    <Layout className=\"h-screen\">\n      <div className=\"relative\">{children || <Outlet />}</div>\n    </Layout>\n  );\n};\n\nexport default ErrorLayout;\n"
  },
  {
    "path": "ui/src/pages/accesses/AccessList.tsx",
    "content": "import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useNavigate, useSearchParams } from \"react-router-dom\";\nimport { IconCirclePlus, IconCopy, IconDots, IconEdit, IconFingerprint, IconPlus, IconReload, IconTrash } from \"@tabler/icons-react\";\nimport { useMount, useRequest } from \"ahooks\";\nimport { App, Avatar, Button, Dropdown, Input, Skeleton, Table, type TableProps, Tabs, Typography, theme } from \"antd\";\nimport dayjs from \"dayjs\";\nimport { produce } from \"immer\";\nimport { ClientResponseError } from \"pocketbase\";\n\nimport AccessEditDrawer, { type AccessEditDrawerProps } from \"@/components/access/AccessEditDrawer\";\nimport CopyableText from \"@/components/CopyableText\";\nimport Empty from \"@/components/Empty\";\nimport Show from \"@/components/Show\";\nimport { type AccessModel } from \"@/domain/access\";\nimport { ACCESS_USAGES, accessProvidersMap } from \"@/domain/provider\";\nimport { useAppSettings, useZustandShallowSelector } from \"@/hooks\";\nimport { get as getAccess } from \"@/repository/access\";\nimport { useAccessesStore } from \"@/stores/access\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\ntype AccessUsages = AccessEditDrawerProps[\"usage\"];\n\nconst AccessList = () => {\n  const navigate = useNavigate();\n  const [searchParams, setSearchParams] = useSearchParams();\n\n  const { t } = useTranslation();\n\n  const { token: themeToken } = theme.useToken();\n\n  const { modal, notification } = App.useApp();\n\n  const { appSettings: globalAppSettings } = useAppSettings();\n\n  const { fetchAccesses, deleteAccess } = useAccessesStore(useZustandShallowSelector([\"loadedAtOnce\", \"fetchAccesses\", \"deleteAccess\"]));\n  useMount(() => {\n    fetchAccesses().catch((err) => {\n      if (err instanceof ClientResponseError && err.isAbort) {\n        return;\n      }\n\n      console.error(err);\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n    });\n  });\n\n  const [filters, setFilters] = useState<Record<string, unknown>>(() => {\n    return {\n      usage: searchParams.get(\"usage\") || (\"dns-hosting\" satisfies AccessUsages),\n      keyword: searchParams.get(\"keyword\"),\n    };\n  });\n  const [page, setPage] = useState<number>(() => parseInt(+searchParams.get(\"page\")! + \"\") || 1);\n  const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get(\"perPage\")! + \"\") || globalAppSettings.defaultPerPage!);\n\n  const [tableData, setTableData] = useState<AccessModel[]>([]);\n  const [tableTotal, setTableTotal] = useState<number>(0);\n  const [tableSelectedRowKeys, setTableSelectedRowKeys] = useState<string[]>([]);\n  const tableColumns: TableProps<AccessModel>[\"columns\"] = [\n    {\n      key: \"id\",\n      title: \"ID\",\n      width: 160,\n      render: (_, record) => {\n        return (\n          <div onClick={(e) => e.stopPropagation()}>\n            <CopyableText className=\"font-mono\">{record.id}</CopyableText>\n          </div>\n        );\n      },\n    },\n    {\n      key: \"name\",\n      title: t(\"access.props.name\"),\n      render: (_, record) => {\n        return (\n          <div className=\"flex max-w-full items-center gap-4 overflow-hidden\">\n            <Avatar shape=\"square\" size={28} src={accessProvidersMap.get(record.provider)?.icon} />\n            <div className=\"flex max-w-full flex-col gap-1 truncate\">\n              <Typography.Text ellipsis>{record.name || \"\\u00A0\"}</Typography.Text>\n              <Typography.Text ellipsis type=\"secondary\">\n                {t(accessProvidersMap.get(record.provider)?.name ?? \"\") || \"\\u00A0\"}\n              </Typography.Text>\n            </div>\n          </div>\n        );\n      },\n    },\n    {\n      key: \"createdAt\",\n      title: t(\"access.props.created_at\"),\n      ellipsis: true,\n      render: (_, record) => {\n        return dayjs(record.created!).format(\"YYYY-MM-DD HH:mm:ss\");\n      },\n    },\n    {\n      key: \"$action\",\n      align: \"end\",\n      fixed: \"right\",\n      width: 64,\n      render: (_, record) => (\n        <Dropdown\n          menu={{\n            items: [\n              {\n                key: \"edit\",\n                label: t(\"access.action.modify.menu\"),\n                icon: (\n                  <span className=\"anticon scale-125\">\n                    <IconEdit size=\"1em\" />\n                  </span>\n                ),\n                onClick: () => {\n                  handleRecordDetailClick(record);\n                },\n              },\n              {\n                key: \"duplicate\",\n                label: t(\"access.action.duplicate.menu\"),\n                icon: (\n                  <span className=\"anticon scale-125\">\n                    <IconCopy size=\"1em\" />\n                  </span>\n                ),\n                onClick: () => {\n                  handleRecordDuplicateClick(record);\n                },\n              },\n              {\n                type: \"divider\",\n              },\n              {\n                key: \"delete\",\n                label: t(\"access.action.delete.menu\"),\n                danger: true,\n                icon: (\n                  <span className=\"anticon scale-125\">\n                    <IconTrash size=\"1em\" />\n                  </span>\n                ),\n                onClick: () => {\n                  handleRecordDeleteClick(record);\n                },\n              },\n            ],\n          }}\n          trigger={[\"click\"]}\n        >\n          <Button icon={<IconDots size=\"1.25em\" />} type=\"text\" />\n        </Dropdown>\n      ),\n      onCell: () => {\n        return {\n          onClick: (e) => {\n            e.stopPropagation();\n          },\n        };\n      },\n    },\n  ];\n  const tableRowSelection: TableProps<AccessModel>[\"rowSelection\"] = {\n    fixed: true,\n    selectedRowKeys: tableSelectedRowKeys,\n    renderCell(checked, _, index, node) {\n      if (!checked) {\n        return (\n          <div className=\"group/selection\">\n            <div className=\"group-hover/selection:hidden\">{(page - 1) * pageSize + index + 1}</div>\n            <div className=\"hidden group-hover/selection:block\">{node}</div>\n          </div>\n        );\n      }\n      return node;\n    },\n    onCell: () => {\n      return {\n        onClick: (e) => {\n          e.stopPropagation();\n        },\n      };\n    },\n    onChange: (keys) => {\n      setTableSelectedRowKeys(keys as string[]);\n    },\n  };\n\n  const {\n    loading,\n    error: loadError,\n    run: refreshData,\n  } = useRequest(\n    async () => {\n      const list = await fetchAccesses();\n      const startIdx = (page - 1) * pageSize;\n      const endIdx = startIdx + pageSize;\n      const items = list\n        .filter((e) => {\n          const keyword = (filters[\"keyword\"] as string | undefined)?.trim();\n          if (keyword) {\n            return e.id === keyword || e.name.includes(keyword);\n          }\n\n          return true;\n        })\n        .filter((e) => {\n          const provider = accessProvidersMap.get(e.provider);\n          switch (filters[\"usage\"] as AccessUsages) {\n            case \"dns\":\n              return !e.reserve && provider?.usages?.includes(ACCESS_USAGES.DNS);\n            case \"hosting\":\n              return !e.reserve && provider?.usages?.includes(ACCESS_USAGES.HOSTING);\n            case \"dns-hosting\":\n              return !e.reserve && (provider?.usages?.includes(ACCESS_USAGES.DNS) || provider?.usages?.includes(ACCESS_USAGES.HOSTING));\n            case \"ca\":\n              return e.reserve === \"ca\" && provider?.usages?.includes(ACCESS_USAGES.CA);\n            case \"notification\":\n              return e.reserve === \"notif\" && provider?.usages?.includes(ACCESS_USAGES.NOTIFICATION);\n          }\n        });\n      return Promise.resolve({\n        items: items.slice(startIdx, endIdx),\n        totalItems: items.length,\n      });\n    },\n    {\n      refreshDeps: [filters, page, pageSize],\n      onBefore: () => {\n        setSearchParams((prev) => {\n          if (filters[\"keyword\"]) {\n            prev.set(\"keyword\", filters[\"keyword\"] as string);\n          } else {\n            prev.delete(\"keyword\");\n          }\n\n          prev.set(\"usage\", filters[\"usage\"] as string);\n\n          prev.set(\"page\", page.toString());\n          prev.set(\"perPage\", pageSize.toString());\n\n          return prev;\n        });\n      },\n      onSuccess: (res) => {\n        setTableData(res.items);\n        setTableTotal(res.totalItems);\n        setTableSelectedRowKeys([]);\n      },\n    }\n  );\n\n  const handleTabChange = (key: string) => {\n    setFilters((prev) => ({ ...prev, usage: key }));\n    setPage(1);\n  };\n\n  const handleSearch = (value: string) => {\n    setFilters((prev) => ({ ...prev, keyword: value }));\n    setPage(1);\n  };\n\n  const handleReloadClick = () => {\n    if (loading) return;\n\n    refreshData();\n  };\n\n  const handlePaginationChange = (page: number, pageSize: number) => {\n    setPage(page);\n    setPageSize(pageSize);\n  };\n\n  const handleCreateClick = () => {\n    navigate(`/accesses/new?usage=${filters[\"usage\"]}`);\n  };\n\n  const { drawerProps: createDrawerProps, ...createDrawer } = AccessEditDrawer.useDrawer();\n  const { drawerProps: detailDrawerProps, ...detailDrawer } = AccessEditDrawer.useDrawer();\n\n  const handleRecordDetailClick = (access: AccessModel) => {\n    const drawer = detailDrawer.open({ data: access, loading: true });\n    getAccess(access.id).then((data) => {\n      drawer.safeUpdate({ data, loading: false });\n    });\n  };\n\n  const handleRecordDuplicateClick = (access: AccessModel) => {\n    const copier = (data: AccessModel) =>\n      produce(data, (draft) => {\n        draft.id = (void 0)!;\n        draft.created = (void 0)!;\n        draft.updated = (void 0)!;\n        draft.name = `${data.name}-copy`;\n        return draft;\n      });\n    getAccess(access.id).then((data) => {\n      createDrawer.open({ data: copier(data) });\n    });\n  };\n\n  const handleRecordDeleteClick = async (access: AccessModel) => {\n    modal.confirm({\n      title: <span className=\"text-error\">{t(\"access.action.delete.modal.title\", { name: access.name })}</span>,\n      content: <span dangerouslySetInnerHTML={{ __html: t(\"access.action.delete.modal.content\") }} />,\n      icon: (\n        <span className=\"anticon\" role=\"img\">\n          <IconTrash className=\"text-error\" size=\"1em\" />\n        </span>\n      ),\n      okText: t(\"common.button.confirm\"),\n      okButtonProps: { danger: true },\n      onOk: async () => {\n        // TODO: 有关联数据的不允许被删除\n        try {\n          await deleteAccess(access);\n          refreshData();\n        } catch (err) {\n          console.error(err);\n          notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n        }\n      },\n    });\n  };\n\n  const handleBatchDeleteClick = () => {\n    const records = tableData.filter((item) => tableSelectedRowKeys.includes(item.id));\n    if (records.length === 0) {\n      return;\n    }\n\n    modal.confirm({\n      title: <span className=\"text-error\">{t(\"access.action.batch_delete.modal.title\")}</span>,\n      content: <span dangerouslySetInnerHTML={{ __html: t(\"access.action.batch_delete.modal.content\", { count: records.length }) }} />,\n      icon: (\n        <span className=\"anticon\" role=\"img\">\n          <IconTrash className=\"text-error\" size=\"1em\" />\n        </span>\n      ),\n      okText: t(\"common.button.confirm\"),\n      okButtonProps: { danger: true },\n      onOk: async () => {\n        try {\n          const resp = await deleteAccess(records);\n          if (resp) {\n            setTableData((prev) => prev.filter((item) => !records.some((record) => record.id === item.id)));\n            setTableTotal((prev) => prev - records.length);\n            refreshData();\n          }\n        } catch (err) {\n          console.error(err);\n          notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n        }\n      },\n    });\n  };\n\n  return (\n    <div className=\"px-6 py-4\">\n      <div className=\"container\">\n        <h1>{t(\"access.page.title\")}</h1>\n        <p className=\"text-base text-gray-500\">{t(\"access.page.subtitle\")}</p>\n      </div>\n\n      <div className=\"container\">\n        <div className=\"flex items-center justify-between gap-x-2 gap-y-3 not-md:flex-col-reverse not-md:items-start not-md:justify-normal\">\n          <div className=\"flex w-full flex-1 items-center gap-x-2 md:max-w-200\">\n            <div className=\"flex-1\">\n              <Input.Search\n                className=\"text-sm placeholder:text-sm\"\n                allowClear\n                defaultValue={filters[\"keyword\"] as string}\n                placeholder={t(\"access.search.placeholder\")}\n                size=\"large\"\n                onSearch={handleSearch}\n              />\n            </div>\n            <div>\n              <Button icon={<IconReload size=\"1.25em\" />} size=\"large\" onClick={handleReloadClick} />\n            </div>\n          </div>\n          <div>\n            <Button className=\"text-sm\" icon={<IconPlus size=\"1.25em\" />} size=\"large\" type=\"primary\" onClick={handleCreateClick}>\n              {t(\"access.action.create.button\")}\n            </Button>\n          </div>\n        </div>\n\n        <Tabs\n          className=\"mt-2 -mb-2\"\n          activeKey={filters[\"usage\"] as string}\n          items={[\n            {\n              key: \"dns-hosting\",\n              label: t(\"access.props.usage.dns_hosting\"),\n            },\n            {\n              key: \"ca\",\n              label: t(\"access.props.usage.ca\"),\n            },\n            {\n              key: \"notification\",\n              label: t(\"access.props.usage.notification\"),\n            },\n          ]}\n          size=\"large\"\n          onChange={(key) => handleTabChange(key)}\n        />\n\n        <div className=\"relative\">\n          <Table<AccessModel>\n            columns={tableColumns}\n            dataSource={tableData}\n            loading={loading}\n            locale={{\n              emptyText: loading ? (\n                <Skeleton />\n              ) : (\n                <Empty\n                  className=\"py-24\"\n                  title={loadError ? t(\"common.text.nodata_failed\") : t(\"access.nodata.title\")}\n                  description={loadError ? unwrapErrMsg(loadError) : t(\"access.nodata.description\")}\n                  icon={<IconFingerprint size={24} />}\n                  extra={\n                    loadError ? (\n                      <Button ghost icon={<IconReload size=\"1.25em\" />} type=\"primary\" onClick={handleReloadClick}>\n                        {t(\"common.button.reload\")}\n                      </Button>\n                    ) : (\n                      <Button icon={<IconCirclePlus size=\"1.25em\" />} type=\"primary\" onClick={handleCreateClick}>\n                        {t(\"access.nodata.button\")}\n                      </Button>\n                    )\n                  }\n                />\n              ),\n            }}\n            pagination={{\n              current: page,\n              pageSize: pageSize,\n              total: tableTotal,\n              showSizeChanger: true,\n              onChange: handlePaginationChange,\n              onShowSizeChange: handlePaginationChange,\n            }}\n            rowClassName=\"cursor-pointer\"\n            rowKey={(record) => record.id}\n            rowSelection={tableRowSelection}\n            scroll={{ x: \"max(100%, 960px)\" }}\n            onRow={(record) => ({\n              onClick: () => {\n                handleRecordDetailClick(record);\n              },\n            })}\n          />\n\n          <Show when={tableSelectedRowKeys.length > 0}>\n            <div\n              className=\"absolute top-0 right-0 left-[32px] z-10 h-[54px]\"\n              style={{\n                left: \"32px\", // Match the width of the table row selection checkbox\n                height: \"54px\", // Match the height of the table header\n                background: themeToken.Table?.headerBg ?? themeToken.colorBgElevated,\n              }}\n            >\n              <div className=\"flex size-full items-center justify-end gap-x-2 overflow-hidden px-4 py-2\">\n                <Button danger ghost onClick={handleBatchDeleteClick}>\n                  {t(\"common.button.delete\")}\n                </Button>\n              </div>\n            </div>\n          </Show>\n        </div>\n\n        <AccessEditDrawer mode=\"create\" usage={filters[\"usage\"] as AccessUsages} {...createDrawerProps} />\n        <AccessEditDrawer mode=\"modify\" usage={filters[\"usage\"] as AccessUsages} {...detailDrawerProps} />\n      </div>\n    </div>\n  );\n};\n\nexport default AccessList;\n"
  },
  {
    "path": "ui/src/pages/accesses/AccessNew.tsx",
    "content": "import { useMemo, useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useNavigate, useSearchParams } from \"react-router-dom\";\nimport { useMount } from \"ahooks\";\nimport { App, Button, Flex, Form } from \"antd\";\n\nimport AccessForm, { type AccessFormUsages } from \"@/components/access/AccessForm\";\nimport AccessProviderPicker, { type AccessProviderPickerInstance } from \"@/components/provider/AccessProviderPicker\";\nimport Show from \"@/components/Show\";\nimport { type AccessModel } from \"@/domain/access\";\nimport { ACCESS_USAGES } from \"@/domain/provider\";\nimport { useZustandShallowSelector } from \"@/hooks\";\nimport { useAccessesStore } from \"@/stores/access\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nconst AccessNew = () => {\n  const navigate = useNavigate();\n  const [searchParams] = useSearchParams();\n\n  const { t } = useTranslation();\n\n  const { notification } = App.useApp();\n\n  const { createAccess } = useAccessesStore(useZustandShallowSelector([\"createAccess\"]));\n\n  const providerUsage = useMemo(() => searchParams.get(\"usage\") as AccessFormUsages, [searchParams]);\n  const providerFilter = AccessForm.useProviderFilterByUsage(providerUsage);\n\n  const providerPickerRef = useRef<AccessProviderPickerInstance>(null);\n  useMount(() => {\n    setTimeout(() => {\n      providerPickerRef.current?.inputRef?.focus();\n    }, 1);\n  });\n\n  const [formInst] = Form.useForm();\n  const [formPending, setFormPending] = useState(false);\n\n  const fieldProvider = Form.useWatch<string>(\"provider\", { form: formInst, preserve: true });\n\n  const handleProviderPick = (value: string) => {\n    formInst.setFieldValue(\"provider\", value);\n  };\n\n  const handleSubmitClick = async () => {\n    let formValues: AccessModel;\n\n    setFormPending(true);\n    try {\n      formValues = await formInst.validateFields();\n      formValues.reserve = providerUsage === \"ca\" ? \"ca\" : providerUsage === \"notification\" ? \"notif\" : void 0;\n    } catch (err) {\n      setFormPending(false);\n      throw err;\n    }\n\n    try {\n      await createAccess(formValues);\n\n      navigate(`/accesses?usage=${providerUsage}`, { replace: true });\n    } catch (err) {\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n\n      throw err;\n    } finally {\n      setFormPending(false);\n    }\n  };\n\n  const handleCancelClick = () => {\n    formInst.resetFields();\n  };\n\n  return (\n    <div className=\"px-6 py-4\">\n      <div className=\"container\">\n        <h1>{t(\"access.new.title\")}</h1>\n        <p className=\"text-base text-gray-500\">{t(\"access.new.subtitle\")}</p>\n      </div>\n\n      <div className=\"container\">\n        <Show when={!fieldProvider}>\n          <AccessProviderPicker\n            ref={providerPickerRef}\n            gap=\"large\"\n            placeholder={t(\"access.form.provider.search.placeholder\")}\n            showOptionTags={\n              providerUsage == null ||\n              (providerUsage === \"dns-hosting\" ? { [\"builtin\"]: true, [ACCESS_USAGES.DNS]: true, [ACCESS_USAGES.HOSTING]: true } : { [\"builtin\"]: true })\n            }\n            showSearch\n            onFilter={providerFilter}\n            onSelect={handleProviderPick}\n          />\n        </Show>\n\n        <div style={{ display: fieldProvider ? \"block\" : \"none\" }}>\n          <div className=\"md:max-w-160\">\n            <AccessForm form={formInst} disabled={formPending} mode=\"create\" usage={providerUsage} />\n          </div>\n          <Flex gap=\"small\">\n            <Button type=\"primary\" onClick={handleSubmitClick}>\n              {t(\"common.button.submit\")}\n            </Button>\n            <Button onClick={handleCancelClick}>{t(\"common.button.cancel\")}</Button>\n          </Flex>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default AccessNew;\n"
  },
  {
    "path": "ui/src/pages/certificates/CertificateList.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useNavigate, useSearchParams } from \"react-router-dom\";\nimport { IconBrowserShare, IconCertificate, IconDots, IconExternalLink, IconReload, IconShieldCancel, IconTrash } from \"@tabler/icons-react\";\nimport { useMount, useRequest } from \"ahooks\";\nimport { App, Button, Dropdown, Input, Segmented, Skeleton, Table, type TableProps, Typography, theme } from \"antd\";\nimport dayjs from \"dayjs\";\nimport { ClientResponseError } from \"pocketbase\";\n\nimport { revoke as revokeCertificate } from \"@/api/certificates\";\nimport CertificateDetailDrawer from \"@/components/certificate/CertificateDetailDrawer\";\nimport Empty from \"@/components/Empty\";\nimport Show from \"@/components/Show\";\nimport { CERTIFICATE_SOURCES, type CertificateModel } from \"@/domain/certificate\";\nimport { useAppSettings, useZustandShallowSelector } from \"@/hooks\";\nimport { get as getCertificate, list as listCertificates, remove as removeCertificate } from \"@/repository/certificate\";\nimport { usePersistenceSettingsStore } from \"@/stores/settings\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nconst CertificateList = () => {\n  const navigate = useNavigate();\n  const [searchParams, setSearchParams] = useSearchParams();\n\n  const { t } = useTranslation();\n\n  const { token: themeToken } = theme.useToken();\n\n  const { message, modal, notification } = App.useApp();\n\n  const { appSettings: globalAppSettings } = useAppSettings();\n\n  const { settings: persistenceSettings, loadSettings: loadPersistenceSettings } = usePersistenceSettingsStore(\n    useZustandShallowSelector([\"settings\", \"loadSettings\"])\n  );\n  useMount(() => loadPersistenceSettings(false));\n\n  const [expiryThreshold, setExpiryThreshold] = useState(() => persistenceSettings.certificatesWarningDaysBeforeExpire || 0);\n  useEffect(() => {\n    setExpiryThreshold(persistenceSettings.certificatesWarningDaysBeforeExpire || 0);\n  }, [persistenceSettings.certificatesWarningDaysBeforeExpire]);\n\n  const [filters, setFilters] = useState<Record<string, unknown>>(() => {\n    return {\n      keyword: searchParams.get(\"keyword\"),\n      state: searchParams.get(\"state\"),\n    };\n  });\n  const [sorter, setSorter] = useState<ArrayElement<Parameters<NonNullable<TableProps<CertificateModel>[\"onChange\"]>>[2]>>(() => {\n    return {};\n  });\n  const [page, setPage] = useState<number>(() => parseInt(+searchParams.get(\"page\")! + \"\") || 1);\n  const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get(\"perPage\")! + \"\") || globalAppSettings.defaultPerPage!);\n\n  const [tableData, setTableData] = useState<CertificateModel[]>([]);\n  const [tableTotal, setTableTotal] = useState<number>(0);\n  const [tableSelectedRowKeys, setTableSelectedRowKeys] = useState<string[]>([]);\n  const tableColumns: TableProps<CertificateModel>[\"columns\"] = [\n    {\n      key: \"name\",\n      title: t(\"certificate.props.subject_alt_names\"),\n      render: (_, record) => <Typography.Text delete={record.isRevoked}>{record.subjectAltNames}</Typography.Text>,\n    },\n    {\n      key: \"validity\",\n      title: t(\"certificate.props.validity\"),\n      sorter: true,\n      sortOrder: sorter.columnKey === \"validity\" ? sorter.order : void 0,\n      render: (_, record) => {\n        const total = dayjs(record.validityNotAfter).diff(dayjs(record.validityNotBefore), \"d\") + 1;\n        const isRevoked = record.isRevoked;\n        const isExpired = dayjs().isAfter(dayjs(record.validityNotAfter));\n        const leftHours = dayjs(record.validityNotAfter).diff(dayjs(), \"h\");\n        const leftDays = Math.round(leftHours / 24);\n\n        return (\n          <div className=\"flex max-w-full flex-col gap-1 truncate\">\n            {!isRevoked && !isExpired ? (\n              leftDays >= expiryThreshold ? (\n                <Typography.Text ellipsis type=\"success\">\n                  <span className=\"mr-2 inline-block size-2 rounded-full bg-success leading-2\">&nbsp;</span>\n                  {t(\"certificate.props.validity.left_days\", { left: leftDays, total })}\n                </Typography.Text>\n              ) : (\n                <Typography.Text ellipsis type=\"warning\">\n                  <span className=\"mr-2 inline-block size-2 rounded-full bg-warning leading-2\">&nbsp;</span>\n                  {leftDays >= 1\n                    ? t(\"certificate.props.validity.left_days\", { left: leftDays, total })\n                    : t(\"certificate.props.validity.less_than_a_day\", { total })}\n                </Typography.Text>\n              )\n            ) : (\n              <Typography.Text ellipsis type=\"danger\">\n                <span className=\"mr-2 inline-block size-2 rounded-full bg-error leading-2\">&nbsp;</span>\n                {isRevoked ? t(\"certificate.props.revoked\") : t(\"certificate.props.validity.expired\")}\n              </Typography.Text>\n            )}\n\n            <Typography.Text ellipsis type=\"secondary\">\n              {t(\"certificate.props.validity.expiration\", { date: dayjs(record.validityNotAfter).format(\"YYYY-MM-DD\") })}\n            </Typography.Text>\n          </div>\n        );\n      },\n    },\n    {\n      key: \"brand\",\n      title: t(\"certificate.props.brand\"),\n      render: (_, record) => (\n        <div className=\"flex max-w-full flex-col gap-1 truncate\">\n          <Typography.Text ellipsis>{record.issuerOrg || \"\\u00A0\"}</Typography.Text>\n          <Typography.Text ellipsis>{record.keyAlgorithm || \"\\u00A0\"}</Typography.Text>\n        </div>\n      ),\n    },\n    {\n      key: \"source\",\n      title: t(\"certificate.props.source\"),\n      render: (_, record) => {\n        const workflowId = record.workflowRef;\n        return (\n          <div className=\"flex max-w-full flex-col gap-1 truncate\">\n            <Typography.Text ellipsis>{t(`certificate.props.source.${record.source}`)}</Typography.Text>\n            <Typography.Link\n              ellipsis\n              type=\"secondary\"\n              onClick={(e) => {\n                e.stopPropagation();\n                if (workflowId) {\n                  navigate(`/workflows/${workflowId}`);\n                }\n              }}\n            >\n              {record.expand?.workflowRef?.name ?? <span className=\"font-mono\">{`#${workflowId}`}</span>}\n            </Typography.Link>\n          </div>\n        );\n      },\n    },\n    {\n      key: \"createdAt\",\n      title: t(\"certificate.props.created_at\"),\n      ellipsis: true,\n      render: (_, record) => {\n        return dayjs(record.created!).format(\"YYYY-MM-DD HH:mm:ss\");\n      },\n    },\n    {\n      key: \"$action\",\n      align: \"end\",\n      fixed: \"right\",\n      width: 64,\n      render: (_, record) => (\n        <Dropdown\n          menu={{\n            items: [\n              {\n                key: \"view\",\n                label: t(\"certificate.action.view.menu\"),\n                icon: (\n                  <span className=\"anticon scale-125\">\n                    <IconBrowserShare size=\"1em\" />\n                  </span>\n                ),\n                onClick: () => {\n                  handleRecordDetailClick(record);\n                },\n              },\n              {\n                key: \"revoke\",\n                label: t(\"certificate.action.revoke.menu\"),\n                danger: true,\n                disabled: record.source !== CERTIFICATE_SOURCES.REQUEST || record.isRevoked,\n                icon: (\n                  <span className=\"anticon scale-125\">\n                    <IconShieldCancel size=\"1em\" />\n                  </span>\n                ),\n                onClick: () => {\n                  handleRecordRevokeClick(record);\n                },\n              },\n              {\n                type: \"divider\",\n              },\n              {\n                key: \"delete\",\n                label: t(\"certificate.action.delete.menu\"),\n                danger: true,\n                icon: (\n                  <span className=\"anticon scale-125\">\n                    <IconTrash size=\"1em\" />\n                  </span>\n                ),\n                onClick: () => {\n                  handleRecordDeleteClick(record);\n                },\n              },\n            ],\n          }}\n          trigger={[\"click\"]}\n        >\n          <Button icon={<IconDots size=\"1.25em\" />} type=\"text\" />\n        </Dropdown>\n      ),\n      onCell: () => {\n        return {\n          onClick: (e) => {\n            e.stopPropagation();\n          },\n        };\n      },\n    },\n  ];\n  const tableRowSelection: TableProps<CertificateModel>[\"rowSelection\"] = {\n    fixed: true,\n    selectedRowKeys: tableSelectedRowKeys,\n    renderCell(checked, _, index, node) {\n      if (!checked) {\n        return (\n          <div className=\"group/selection\">\n            <div className=\"group-hover/selection:hidden\">{(page - 1) * pageSize + index + 1}</div>\n            <div className=\"hidden group-hover/selection:block\">{node}</div>\n          </div>\n        );\n      }\n      return node;\n    },\n    onCell: () => {\n      return {\n        onClick: (e) => {\n          e.stopPropagation();\n        },\n      };\n    },\n    onChange: (keys) => {\n      setTableSelectedRowKeys(keys as string[]);\n    },\n  };\n\n  const {\n    loading,\n    error: loadError,\n    run: refreshData,\n  } = useRequest(\n    () => {\n      const { columnKey: sorterKey, order: sorterOrder } = sorter;\n      let sort: string | undefined;\n      sort = sorterKey === \"validity\" ? \"validityNotAfter\" : void 0;\n      sort = sort && (sorterOrder === \"ascend\" ? `${sort}` : sorterOrder === \"descend\" ? `-${sort}` : void 0);\n\n      return listCertificates({\n        keyword: filters[\"keyword\"] as string,\n        state: filters[\"state\"] as Parameters<typeof listCertificates>[0][\"state\"],\n        stateThreshold: expiryThreshold,\n        sort: sort,\n        page: page,\n        perPage: pageSize,\n      });\n    },\n    {\n      refreshDeps: [expiryThreshold, filters, sorter, page, pageSize],\n      onBefore: async () => {\n        setSearchParams((prev) => {\n          if (filters[\"keyword\"]) {\n            prev.set(\"keyword\", filters[\"keyword\"] as string);\n          } else {\n            prev.delete(\"keyword\");\n          }\n\n          if (filters[\"state\"]) {\n            prev.set(\"state\", filters[\"state\"] as string);\n          } else {\n            prev.delete(\"state\");\n          }\n\n          prev.set(\"page\", page.toString());\n          prev.set(\"perPage\", pageSize.toString());\n\n          return prev;\n        });\n      },\n      onSuccess: (res) => {\n        setTableData(res.items);\n        setTableTotal(res.totalItems);\n        setTableSelectedRowKeys([]);\n      },\n      onError: (err) => {\n        if (err instanceof ClientResponseError && err.isAbort) {\n          return;\n        }\n\n        console.error(err);\n        notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n\n        throw err;\n      },\n    }\n  );\n\n  const handleSearch = (value: string) => {\n    setFilters((prev) => ({ ...prev, keyword: value.trim() }));\n    setPage(1);\n  };\n\n  const handleReloadClick = () => {\n    if (loading) return;\n\n    refreshData();\n  };\n\n  const handlePaginationChange = (page: number, pageSize: number) => {\n    setPage(page);\n    setPageSize(pageSize);\n  };\n\n  const { drawerProps: detailDrawerProps, ...detailDrawer } = CertificateDetailDrawer.useDrawer();\n\n  const handleRecordDetailClick = (certificate: CertificateModel) => {\n    const drawer = detailDrawer.open({ data: certificate, loading: true });\n    getCertificate(certificate.id).then((data) => {\n      drawer.safeUpdate({ data, loading: false });\n    });\n  };\n\n  const handleRecordRevokeClick = (certificate: CertificateModel) => {\n    modal.confirm({\n      title: <span className=\"text-error\">{t(\"certificate.action.revoke.modal.title\", { name: certificate.subjectAltNames })}</span>,\n      content: <span dangerouslySetInnerHTML={{ __html: t(\"certificate.action.revoke.modal.content\") }} />,\n      icon: (\n        <span className=\"anticon\" role=\"img\">\n          <IconShieldCancel className=\"text-error\" size=\"1em\" />\n        </span>\n      ),\n      okText: t(\"common.button.confirm\"),\n      okButtonProps: { danger: true },\n      onOk: async () => {\n        try {\n          const resp = await revokeCertificate(certificate.id);\n          if (resp) {\n            message.success(t(\"common.text.operation_succeeded\"));\n            setTableData((prev) => prev.map((item) => (item.id === certificate.id ? { ...item, isRevoked: true } : item)));\n            refreshData();\n          }\n        } catch (err) {\n          console.error(err);\n          notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n        }\n      },\n    });\n  };\n\n  const handleRecordDeleteClick = (certificate: CertificateModel) => {\n    modal.confirm({\n      title: <span className=\"text-error\">{t(\"certificate.action.delete.modal.title\", { name: certificate.subjectAltNames })}</span>,\n      content: <span dangerouslySetInnerHTML={{ __html: t(\"certificate.action.delete.modal.content\") }} />,\n      icon: (\n        <span className=\"anticon\" role=\"img\">\n          <IconTrash className=\"text-error\" size=\"1em\" />\n        </span>\n      ),\n      okText: t(\"common.button.confirm\"),\n      okButtonProps: { danger: true },\n      onOk: async () => {\n        try {\n          const resp = await removeCertificate(certificate);\n          if (resp) {\n            setTableSelectedRowKeys((prev) => prev.filter((key) => key !== certificate.id));\n            setTableData((prev) => prev.filter((item) => item.id !== certificate.id));\n            setTableTotal((prev) => prev - 1);\n            refreshData();\n          }\n        } catch (err) {\n          console.error(err);\n          notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n        }\n      },\n    });\n  };\n\n  const handleBatchDeleteClick = () => {\n    const records = tableData.filter((item) => tableSelectedRowKeys.includes(item.id));\n    if (records.length === 0) {\n      return;\n    }\n\n    modal.confirm({\n      title: <span className=\"text-error\">{t(\"certificate.action.batch_delete.modal.title\")}</span>,\n      content: <span dangerouslySetInnerHTML={{ __html: t(\"certificate.action.batch_delete.modal.content\", { count: records.length }) }} />,\n      icon: (\n        <span className=\"anticon\" role=\"img\">\n          <IconTrash className=\"text-error\" size=\"1em\" />\n        </span>\n      ),\n      okText: t(\"common.button.confirm\"),\n      okButtonProps: { danger: true },\n      onOk: async () => {\n        try {\n          const resp = await removeCertificate(records);\n          if (resp) {\n            setTableData((prev) => prev.filter((item) => !records.some((record) => record.id === item.id)));\n            setTableTotal((prev) => prev - records.length);\n            refreshData();\n          }\n        } catch (err) {\n          console.error(err);\n          notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n        }\n      },\n    });\n  };\n\n  return (\n    <div className=\"px-6 py-4\">\n      <div className=\"container\">\n        <h1>{t(\"certificate.page.title\")}</h1>\n        <p className=\"text-base text-gray-500\">{t(\"certificate.page.subtitle\")}</p>\n      </div>\n\n      <div className=\"container\">\n        <div className=\"flex items-center justify-between gap-x-2 gap-y-3 not-md:flex-col-reverse not-md:items-start not-md:justify-normal\">\n          <div className=\"flex w-full flex-1 items-center gap-x-2 md:max-w-200\">\n            <div>\n              <Segmented\n                options={[\n                  { label: <span className=\"text-sm\">{t(\"certificate.props.validity.filter.all\")}</span>, value: \"\" },\n                  { label: <span className=\"text-sm\">{t(\"certificate.props.validity.filter.expiring_soon\")}</span>, value: \"expiringSoon\" },\n                  { label: <span className=\"text-sm\">{t(\"certificate.props.validity.filter.expired\")}</span>, value: \"expired\" },\n                ]}\n                size=\"large\"\n                value={(filters[\"state\"] as string) || \"\"}\n                onChange={(value) => {\n                  setPage(1);\n                  setFilters((prev) => ({ ...prev, state: value }));\n                }}\n              />\n            </div>\n            <div className=\"flex-1\">\n              <Input.Search\n                className=\"text-sm placeholder:text-sm\"\n                allowClear\n                defaultValue={filters[\"keyword\"] as string}\n                placeholder={t(\"certificate.search.placeholder\")}\n                size=\"large\"\n                onSearch={handleSearch}\n              />\n            </div>\n            <div>\n              <Button icon={<IconReload size=\"1.25em\" />} size=\"large\" onClick={handleReloadClick} />\n            </div>\n          </div>\n          <div></div>\n        </div>\n\n        <div className=\"relative mt-4\">\n          <Table<CertificateModel>\n            columns={tableColumns}\n            dataSource={tableData}\n            loading={loading}\n            locale={{\n              emptyText: loading ? (\n                <Skeleton />\n              ) : (\n                <Empty\n                  className=\"py-24\"\n                  title={loadError ? t(\"common.text.nodata_failed\") : t(\"certificate.nodata.title\")}\n                  description={loadError ? unwrapErrMsg(loadError) : t(\"certificate.nodata.description\")}\n                  icon={<IconCertificate size={24} />}\n                  extra={\n                    loadError ? (\n                      <Button ghost icon={<IconReload size=\"1.25em\" />} type=\"primary\" onClick={handleReloadClick}>\n                        {t(\"common.button.reload\")}\n                      </Button>\n                    ) : (\n                      <Button icon={<IconExternalLink size=\"1.25em\" />} type=\"primary\" onClick={() => navigate(\"/workflows\")}>\n                        {t(\"certificate.nodata.button\")}\n                      </Button>\n                    )\n                  }\n                />\n              ),\n            }}\n            pagination={{\n              current: page,\n              pageSize: pageSize,\n              total: tableTotal,\n              showSizeChanger: true,\n              onChange: handlePaginationChange,\n              onShowSizeChange: handlePaginationChange,\n            }}\n            rowClassName=\"cursor-pointer\"\n            rowKey={(record) => record.id}\n            rowSelection={tableRowSelection}\n            scroll={{ x: \"max(100%, 960px)\" }}\n            onChange={(_, __, sorter) => {\n              setSorter(Array.isArray(sorter) ? sorter[0] : sorter);\n            }}\n            onRow={(record) => ({\n              onClick: () => {\n                handleRecordDetailClick(record);\n              },\n            })}\n          />\n\n          <Show when={tableSelectedRowKeys.length > 0}>\n            <div\n              className=\"absolute top-0 right-0 left-[32px] z-10 h-[54px]\"\n              style={{\n                left: \"32px\", // Match the width of the table row selection checkbox\n                height: \"54px\", // Match the height of the table header\n                background: themeToken.Table?.headerBg ?? themeToken.colorBgElevated,\n              }}\n            >\n              <div className=\"flex size-full items-center justify-end gap-x-2 overflow-hidden px-4 py-2\">\n                <Button danger ghost onClick={handleBatchDeleteClick}>\n                  {t(\"common.button.delete\")}\n                </Button>\n              </div>\n            </div>\n          </Show>\n        </div>\n\n        <CertificateDetailDrawer {...detailDrawerProps} />\n      </div>\n    </div>\n  );\n};\n\nexport default CertificateList;\n"
  },
  {
    "path": "ui/src/pages/dashboard/Dashboard.tsx",
    "content": "import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useNavigate } from \"react-router-dom\";\nimport {\n  IconActivity,\n  IconAlertHexagon,\n  IconBox,\n  IconCertificate,\n  IconCirclePlus,\n  IconConfetti,\n  IconExternalLink,\n  IconHexagonLetterX,\n  IconHistory,\n  IconLock,\n  IconPlugConnected,\n  IconReload,\n  IconRoute,\n  IconShieldCheckered,\n} from \"@tabler/icons-react\";\nimport { useRequest } from \"ahooks\";\nimport { App, Button, Card, Col, Row, Skeleton, Table, type TableProps, Typography } from \"antd\";\nimport dayjs from \"dayjs\";\nimport { ClientResponseError } from \"pocketbase\";\n\nimport { get as getStatistics } from \"@/api/statistics\";\nimport Empty from \"@/components/Empty\";\nimport WorkflowRunDetailDrawer from \"@/components/workflow/WorkflowRunDetailDrawer\";\nimport WorkflowStatus from \"@/components/workflow/WorkflowStatus\";\nimport { APP_DOWNLOAD_URL } from \"@/domain/app\";\nimport { type Statistics } from \"@/domain/statistics\";\nimport { type WorkflowRunModel } from \"@/domain/workflowRun\";\nimport { useBrowserTheme, useVersionChecker } from \"@/hooks\";\nimport { get as getWorkflowRun, list as listWorkflowRuns } from \"@/repository/workflowRun\";\nimport { mergeCls } from \"@/utils/css\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nconst Dashboard = () => {\n  const { t } = useTranslation();\n\n  return (\n    <div className=\"px-6 py-4\">\n      <div className=\"container\">\n        <h1>{t(\"dashboard.page.title\")}</h1>\n      </div>\n\n      <div className=\"container\">\n        <div className=\"my-1.5\">\n          <StatisticCards />\n        </div>\n\n        <div className=\"mt-8\">\n          <h3>{t(\"dashboard.shortcut\")}</h3>\n          <Shortcuts />\n        </div>\n\n        <div className=\"mt-8\">\n          <h3>{t(\"dashboard.recent_workflow_runs\")}</h3>\n          <WorkflowRunHistoryTable />\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst StatisticCard = ({\n  className,\n  style,\n  label,\n  loading,\n  icon,\n  value,\n  onClick,\n}: {\n  className?: string;\n  style?: React.CSSProperties;\n  label: React.ReactNode;\n  loading?: boolean;\n  icon: React.ReactNode;\n  value?: string | number | React.ReactNode;\n  onClick?: () => void;\n}) => {\n  return (\n    <Card\n      className={mergeCls(\"size-full overflow-hidden \", className)}\n      style={style}\n      styles={{ body: { padding: 0 } }}\n      hoverable\n      loading={loading}\n      variant=\"borderless\"\n      onClick={onClick}\n    >\n      <div className=\"relative overflow-hidden pt-6 pr-4 pb-4 pl-6\">\n        <div className=\"absolute inset-0 z-0 bg-stone-200 opacity-10\">\n          <div\n            className=\"size-full\"\n            style={{\n              backgroundImage:\n                \"linear-gradient(rgba(255, 255, 255, 0.8) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.8) 1px, transparent 1px)\",\n              backgroundSize: \"20px 20px\",\n            }}\n          />\n        </div>\n        <div className=\"mb-2\">\n          <div className=\"truncate text-sm font-medium text-white/75\">{label}</div>\n        </div>\n        <div className=\"relative flex items-center justify-between\">\n          <div className=\"truncate text-4xl font-medium text-white\">{value}</div>\n          <div className=\"flex size-12 items-center justify-center rounded-full bg-white/25 p-3 text-white/75\">{icon}</div>\n        </div>\n      </div>\n    </Card>\n  );\n};\n\nconst StatisticCards = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {\n  const navigate = useNavigate();\n\n  const { t } = useTranslation();\n\n  const { theme: browserTheme } = useBrowserTheme();\n\n  const { notification } = App.useApp();\n\n  const cardGridSpans = {\n    xs: { flex: \"50%\" },\n    md: { flex: \"50%\" },\n    lg: { flex: \"33.3333%\" },\n    xl: { flex: \"33.3333%\" },\n    xxl: { flex: \"20%\" },\n  };\n  const cardStylesFn = (color: string) => ({\n    background:\n      browserTheme === \"dark\"\n        ? `linear-gradient(135deg, color-mix(in srgb, ${color} 50%, black 20%) 0%, color-mix(in srgb, ${color} 50%, white 20%) 100%)`\n        : `linear-gradient(135deg, color-mix(in srgb, ${color} 80%, black 30%) 0%, color-mix(in srgb, ${color} 80%, white 30%) 100%)`,\n  });\n\n  const [statistics, setStatistics] = useState<Statistics>();\n\n  const { loading } = useRequest(\n    () => {\n      return getStatistics();\n    },\n    {\n      onSuccess: (res) => {\n        setStatistics(res.data);\n      },\n      onError: (err) => {\n        if (err instanceof ClientResponseError && err.isAbort) {\n          return;\n        }\n\n        console.error(err);\n        notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n\n        throw err;\n      },\n    }\n  );\n\n  return (\n    <div className={className} style={style}>\n      <Row className=\"justify-stretch\" gutter={[16, 16]}>\n        <Col className=\"overflow-hidden\" {...cardGridSpans}>\n          <StatisticCard\n            style={cardStylesFn(\"var(--color-info)\")}\n            icon={<IconShieldCheckered size={48} />}\n            label={t(\"dashboard.statistics.all_certificates\")}\n            loading={loading}\n            value={statistics?.certificateTotal ?? \"-\"}\n            onClick={() => navigate(\"/certificates\")}\n          />\n        </Col>\n        <Col className=\"overflow-hidden\" {...cardGridSpans}>\n          <StatisticCard\n            style={cardStylesFn(\"var(--color-warning)\")}\n            icon={<IconAlertHexagon size={48} />}\n            label={t(\"dashboard.statistics.expiring_soon_certificates\")}\n            loading={loading}\n            value={statistics?.certificateExpiringSoon ?? \"-\"}\n            onClick={() => navigate(\"/certificates?state=expiringSoon\")}\n          />\n        </Col>\n        <Col className=\"overflow-hidden\" {...cardGridSpans}>\n          <StatisticCard\n            style={cardStylesFn(\"var(--color-error)\")}\n            icon={<IconHexagonLetterX size={48} />}\n            label={t(\"dashboard.statistics.expired_certificates\")}\n            loading={loading}\n            value={statistics?.certificateExpired ?? \"-\"}\n            onClick={() => navigate(\"/certificates?state=expired\")}\n          />\n        </Col>\n        <Col className=\"overflow-hidden\" {...cardGridSpans}>\n          <StatisticCard\n            style={cardStylesFn(\"var(--color-info)\")}\n            icon={<IconRoute size={48} />}\n            label={t(\"dashboard.statistics.all_workflows\")}\n            loading={loading}\n            value={statistics?.workflowTotal ?? \"-\"}\n            onClick={() => navigate(\"/workflows\")}\n          />\n        </Col>\n        <Col className=\"overflow-hidden\" {...cardGridSpans}>\n          <StatisticCard\n            style={cardStylesFn(\"var(--color-success)\")}\n            icon={<IconActivity size={48} />}\n            label={t(\"dashboard.statistics.enabled_workflows\")}\n            loading={loading}\n            value={statistics?.workflowEnabled ?? \"-\"}\n            onClick={() => navigate(\"/workflows?state=enabled\")}\n          />\n        </Col>\n      </Row>\n    </div>\n  );\n};\n\nconst Shortcuts = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {\n  const navigate = useNavigate();\n\n  const { t } = useTranslation();\n\n  const { hasUpdate } = useVersionChecker();\n\n  return (\n    <div className={className} style={style}>\n      <div className=\"flex items-center gap-4 not-md:flex-wrap\">\n        <Button\n          className=\"shadow-sm\"\n          icon={<IconCirclePlus color=\"var(--color-primary)\" size=\"1.25em\" />}\n          shape=\"round\"\n          size=\"large\"\n          onClick={() => navigate(\"/workflows/new\")}\n        >\n          <span className=\"text-sm\">{t(\"dashboard.shortcut.create_workflow\")}</span>\n        </Button>\n        <Button\n          className=\"shadow-sm\"\n          icon={<IconLock color=\"var(--color-warning)\" size=\"1.25em\" />}\n          shape=\"round\"\n          size=\"large\"\n          onClick={() => navigate(\"/settings/account\")}\n        >\n          <span className=\"text-sm\">{t(\"dashboard.shortcut.change_account\")}</span>\n        </Button>\n        <Button\n          className=\"shadow-sm\"\n          icon={<IconPlugConnected color=\"var(--color-info)\" size=\"1.25em\" />}\n          shape=\"round\"\n          size=\"large\"\n          onClick={() => navigate(\"/settings/ssl-provider\")}\n        >\n          <span className=\"text-sm\">{t(\"dashboard.shortcut.configure_ca\")}</span>\n        </Button>\n        {hasUpdate && (\n          <Button\n            className=\"shadow-sm\"\n            icon={<IconConfetti className=\"animate-bounce\" color=\"var(--color-error)\" size=\"1.25em\" />}\n            shape=\"round\"\n            size=\"large\"\n            onClick={() => window.open(APP_DOWNLOAD_URL, \"_blank\")}\n          >\n            <span className=\"text-sm\">{t(\"dashboard.shortcut.upgrade\")}</span>\n          </Button>\n        )}\n      </div>\n    </div>\n  );\n};\n\nconst WorkflowRunHistoryTable = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {\n  const navigate = useNavigate();\n\n  const { t } = useTranslation();\n\n  const { notification } = App.useApp();\n\n  const [tableData, setTableData] = useState<WorkflowRunModel[]>([]);\n  const tableColumns: TableProps<WorkflowRunModel>[\"columns\"] = [\n    {\n      key: \"$index\",\n      align: \"center\",\n      fixed: \"left\",\n      width: 48,\n      render: (_, __, index) => index + 1,\n    },\n    {\n      key: \"id\",\n      title: \"ID\",\n      width: 160,\n      render: (_, record) => <span className=\"font-mono\">{record.id}</span>,\n    },\n    {\n      key: \"workflow\",\n      title: t(\"workflow_run.props.workflow\"),\n      render: (_, record) => {\n        const workflow = record.expand?.workflowRef;\n        return (\n          <div className=\"max-w-full truncate\">\n            <Typography.Link\n              ellipsis\n              onClick={() => {\n                if (workflow) {\n                  navigate(`/workflows/${workflow.id}`);\n                }\n              }}\n            >\n              {workflow?.name ?? <span className=\"font-mono\">{`#${record.workflowRef}`}</span>}\n            </Typography.Link>\n          </div>\n        );\n      },\n    },\n    {\n      key: \"status\",\n      title: t(\"workflow_run.props.status\"),\n      render: (_, record) => {\n        return <WorkflowStatus type=\"filled\" value={record.status} />;\n      },\n    },\n    {\n      key: \"startedAt\",\n      title: t(\"workflow_run.props.started_at\"),\n      ellipsis: true,\n      render: (_, record) => {\n        if (record.startedAt) {\n          return dayjs(record.startedAt).format(\"YYYY-MM-DD HH:mm:ss\");\n        }\n\n        return <></>;\n      },\n    },\n    {\n      key: \"endedAt\",\n      title: t(\"workflow_run.props.ended_at\"),\n      ellipsis: true,\n      render: (_, record) => {\n        if (record.endedAt) {\n          return dayjs(record.endedAt).format(\"YYYY-MM-DD HH:mm:ss\");\n        }\n\n        return <></>;\n      },\n    },\n    {\n      key: \"artifacts\",\n      title: t(\"workflow_run.props.artifacts\"),\n      width: 160,\n      render: (_, record) => {\n        if (record.outputs && record.outputs.length > 0) {\n          const keys = new Set<string>();\n          const icons: React.ReactNode[] = [];\n\n          for (const output of record.outputs) {\n            if (output.type === \"ref\" && output.value?.split(\"#\")?.at(0) === \"certificate\") {\n              const KEY = \"certificate\";\n              if (keys.has(KEY)) continue;\n\n              keys.add(KEY);\n              icons.push(<IconCertificate key={KEY} size=\"1.25em\" />);\n            } else {\n              const KEY = \"other\";\n              if (keys.has(KEY)) continue;\n\n              keys.add(KEY);\n              icons.push(<IconBox key={KEY} size=\"1.25em\" />);\n            }\n          }\n\n          return <div className=\"flex items-center gap-2\">{icons}</div>;\n        }\n\n        return <></>;\n      },\n    },\n  ];\n\n  const {\n    loading,\n    error: loadError,\n    run: refreshData,\n  } = useRequest(\n    () => {\n      return listWorkflowRuns({\n        page: 1,\n        perPage: 15,\n        expand: true,\n      });\n    },\n    {\n      onSuccess: (res) => {\n        setTableData(res.items);\n      },\n      onError: (err) => {\n        if (err instanceof ClientResponseError && err.isAbort) {\n          return;\n        }\n\n        console.error(err);\n        notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n\n        throw err;\n      },\n    }\n  );\n\n  const handleReloadClick = () => {\n    if (loading) return;\n\n    refreshData();\n  };\n\n  const { drawerProps: detailDrawerProps, ...detailDrawer } = WorkflowRunDetailDrawer.useDrawer();\n\n  const handleRecordDetailClick = (workflowRun: WorkflowRunModel) => {\n    const drawer = detailDrawer.open({ data: workflowRun, loading: true });\n    getWorkflowRun(workflowRun.id).then((data) => {\n      drawer.safeUpdate({ data, loading: false });\n    });\n  };\n\n  return (\n    <div className={className} style={style}>\n      <Table<WorkflowRunModel>\n        columns={tableColumns}\n        dataSource={tableData}\n        loading={loading}\n        locale={{\n          emptyText: loading ? (\n            <Skeleton />\n          ) : (\n            <Empty\n              className=\"py-24\"\n              title={loadError ? t(\"common.text.nodata_failed\") : t(\"common.text.nodata\")}\n              description={loadError ? unwrapErrMsg(loadError) : t(\"dashboard.recent_workflow_runs.nodata.description\")}\n              icon={<IconHistory size={24} />}\n              extra={\n                loadError ? (\n                  <Button ghost icon={<IconReload size=\"1.25em\" />} type=\"primary\" onClick={handleReloadClick}>\n                    {t(\"common.button.reload\")}\n                  </Button>\n                ) : (\n                  <Button icon={<IconExternalLink size=\"1.25em\" />} type=\"primary\" onClick={() => navigate(\"/workflows\")}>\n                    {t(\"dashboard.recent_workflow_runs.nodata.button\")}\n                  </Button>\n                )\n              }\n            />\n          ),\n        }}\n        pagination={false}\n        rowClassName=\"cursor-pointer\"\n        rowKey={(record) => record.id}\n        scroll={{ x: \"max(100%, 720px)\" }}\n        onRow={(record) => ({\n          onClick: () => {\n            handleRecordDetailClick(record);\n          },\n        })}\n      />\n\n      <WorkflowRunDetailDrawer {...detailDrawerProps} />\n    </div>\n  );\n};\n\nexport default Dashboard;\n"
  },
  {
    "path": "ui/src/pages/login/Login.tsx",
    "content": "import { useMemo } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useNavigate } from \"react-router-dom\";\nimport { IconArrowRight, IconLock, IconMail } from \"@tabler/icons-react\";\nimport { App, Button, Card, Divider, Form, Input, Space } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport AppDocument from \"@/components/AppDocument\";\nimport AppLocale from \"@/components/AppLocale\";\nimport AppTheme from \"@/components/AppTheme\";\nimport AppVersion from \"@/components/AppVersion\";\nimport { useAntdForm, useBrowserTheme } from \"@/hooks\";\n\nimport { authWithPassword } from \"@/repository/admin\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nconst Login = () => {\n  const navigage = useNavigate();\n\n  const { t } = useTranslation();\n\n  const { notification } = App.useApp();\n  const { theme: browserTheme } = useBrowserTheme();\n\n  const bgStyle = useMemo<React.CSSProperties>(() => {\n    let svg = \"\";\n    let mask = \"\";\n    if (browserTheme === \"dark\") {\n      svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\" width=\"32\" height=\"32\" fill=\"none\" stroke=\"rgb(202 78 13 / 0.12)\"><path d=\"M0 .5H31.5V32\"/></svg>`;\n      mask = \"white\";\n    } else {\n      svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\" width=\"32\" height=\"32\" fill=\"none\" stroke=\"rgb(249 115 22 / 0.08)\"><path d=\"M0 .5H31.5V32\"/></svg>`;\n      mask = \"black\";\n    }\n\n    return {\n      backgroundImage: `url('data:image/svg+xml;base64,${btoa(svg)}')`,\n      maskImage: `linear-gradient(to bottom right, transparent, ${mask}, transparent)`,\n    };\n  }, [browserTheme]);\n\n  const formSchema = z.object({\n    username: z.email(t(\"login.username.errmsg.invalid\")),\n    password: z.string().min(10, t(\"login.password.errmsg.invalid\")),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const {\n    form: formInst,\n    formPending,\n    formProps,\n  } = useAntdForm<z.infer<typeof formSchema>>({\n    initialValues: {\n      username: \"\",\n      password: \"\",\n    },\n    onSubmit: async (values) => {\n      try {\n        await authWithPassword(values.username, values.password);\n        await navigage(\"/\");\n      } catch (err) {\n        notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n\n        throw err;\n      }\n    },\n  });\n\n  return (\n    <>\n      <div className=\"pointer-events-none fixed min-h-screen w-full\" style={bgStyle}></div>\n\n      <div className=\"flex h-screen w-full flex-col items-center justify-center\">\n        <Card className=\"w-120 max-w-full rounded-md shadow-md max-sm:h-full max-sm:w-full max-sm:rounded-none\">\n          <div className=\"px-4 py-8\">\n            <div className=\"mb-12 flex items-center justify-center\">\n              <img src=\"/logo.svg\" className=\"w-16\" />\n            </div>\n\n            <Form {...formProps} form={formInst} disabled={formPending} layout=\"vertical\" validateTrigger=\"onBlur\">\n              <Form.Item name=\"username\" label={t(\"login.username.label\")} rules={[formRule]}>\n                <Space.Compact block>\n                  <Space.Addon>\n                    <IconMail size=\"1.25em\" />\n                  </Space.Addon>\n                  <Input autoComplete=\"new-password\" autoFocus placeholder={t(\"login.username.placeholder\")} size=\"large\" />\n                </Space.Compact>\n              </Form.Item>\n\n              <Form.Item name=\"password\" label={t(\"login.password.label\")} rules={[formRule]}>\n                <Space.Compact block>\n                  <Space.Addon>\n                    <IconLock size=\"1.25em\" />\n                  </Space.Addon>\n                  <Input.Password autoComplete=\"new-password\" placeholder={t(\"login.password.placeholder\")} size=\"large\" />\n                </Space.Compact>\n              </Form.Item>\n\n              <Form.Item className=\"mt-8 mb-0\">\n                <Button block type=\"primary\" htmlType=\"submit\" icon={<IconArrowRight size=\"1.25em\" />} iconPlacement=\"end\" loading={formPending} size=\"large\">\n                  {t(\"login.submit\")}\n                </Button>\n              </Form.Item>\n            </Form>\n\n            <div className=\"mt-12\">\n              <div className=\"block max-sm:hidden\">\n                <div className=\"flex items-center justify-center\">\n                  <Space align=\"center\" separator={<Divider orientation=\"vertical\" />} size={4}>\n                    <AppLocale.LinkButton />\n                    <AppTheme.LinkButton />\n                    <AppDocument.LinkButton />\n                    <AppVersion.LinkButton />\n                  </Space>\n                </div>\n              </div>\n              <div className=\"hidden max-sm:block\">\n                <div className=\"flex items-center justify-center\">\n                  <Space align=\"center\" separator={<Divider orientation=\"vertical\" />} size={4}>\n                    <AppLocale.LinkButton />\n                    <AppTheme.LinkButton />\n                    <AppDocument.LinkButton />\n                  </Space>\n                </div>\n                <div className=\"mt-6 flex items-center justify-center\">\n                  <Space align=\"center\" separator={<Divider orientation=\"vertical\" />} size={4}>\n                    <AppVersion.LinkButton />\n                  </Space>\n                </div>\n              </div>\n            </div>\n          </div>\n        </Card>\n      </div>\n    </>\n  );\n};\n\nexport default Login;\n"
  },
  {
    "path": "ui/src/pages/presets/PresetList.tsx",
    "content": "﻿import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useSearchParams } from \"react-router-dom\";\nimport { Tabs } from \"antd\";\n\nimport Show from \"@/components/Show\";\n\nimport PresetListNotifyTemplates from \"./PresetListNotifyTemplates\";\nimport PresetListScriptTemplates from \"./PresetListScriptTemplates\";\n\ntype PresetUsages = \"notification\" | \"script\";\n\nconst PresetList = () => {\n  const [searchParams, setSearchParams] = useSearchParams();\n\n  const { t } = useTranslation();\n\n  const [tabKey, setTabKey] = useState<PresetUsages>(() => {\n    return (searchParams.get(\"usage\") || \"notification\") as PresetUsages;\n  });\n\n  const handleTabChange = (key: string) => {\n    setTabKey(key as PresetUsages);\n    setSearchParams((prev) => {\n      prev.set(\"usage\", key);\n      return prev;\n    });\n  };\n\n  return (\n    <div className=\"px-6 py-4\">\n      <div className=\"container\">\n        <h1>{t(\"preset.page.title\")}</h1>\n        <p className=\"text-base text-gray-500\">{t(\"preset.page.subtitle\")}</p>\n      </div>\n\n      <div className=\"container\">\n        <Tabs\n          className=\"-mt-2\"\n          activeKey={tabKey}\n          items={[\n            {\n              key: \"notification\",\n              label: t(\"preset.props.usage.notification\"),\n            },\n            {\n              key: \"script\",\n              label: t(\"preset.props.usage.script\"),\n            },\n          ]}\n          size=\"large\"\n          onChange={(key) => handleTabChange(key)}\n        />\n\n        <div className=\"relative\">\n          <Show>\n            <Show.Case when={tabKey === \"notification\"}>\n              <PresetListNotifyTemplates />\n            </Show.Case>\n            <Show.Case when={tabKey === \"script\"}>\n              <PresetListScriptTemplates />\n            </Show.Case>\n          </Show>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default PresetList;\n"
  },
  {
    "path": "ui/src/pages/presets/PresetListNotifyTemplates.tsx",
    "content": "﻿import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { IconDots, IconEdit, IconPlus, IconTrash } from \"@tabler/icons-react\";\nimport { useControllableValue, useMount } from \"ahooks\";\nimport { App, Button, Card, Divider, Dropdown, Form, Input, Typography } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { nanoid } from \"nanoid/non-secure\";\nimport { ClientResponseError } from \"pocketbase\";\nimport { z } from \"zod\";\n\nimport DrawerForm from \"@/components/DrawerForm\";\nimport Tips from \"@/components/Tips\";\nimport { useAntdForm, useZustandShallowSelector } from \"@/hooks\";\nimport { useNotifyTemplatesStore } from \"@/stores/settings\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nconst MAX_TEMPLATE_COUNT = 99;\n\ntype PresetTemplate = {\n  name: string;\n  subject: string;\n  message: string;\n};\n\nconst PresetListNotifyTemplates = () => {\n  const { t } = useTranslation();\n\n  const { message, modal, notification } = App.useApp();\n\n  const { templates, loading, loadedAtOnce, fetchTemplates, setTemplates, addTemplate, removeTemplateByIndex } = useNotifyTemplatesStore();\n  useMount(() => {\n    fetchTemplates().catch((err) => {\n      if (err instanceof ClientResponseError && err.isAbort) {\n        return;\n      }\n\n      console.error(err);\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n    });\n  });\n\n  const [createDrawerOpen, setCreateDrawerOpen] = useState(false);\n  const [detailDrawerOpen, setDetailDrawerOpen] = useState(false);\n  const [detailDrawerRecord, setDetailDrawerRecord] = useState<PresetTemplate>();\n  const [detailDrawerIndex, setDetailDrawerIndex] = useState<number>();\n\n  const handleCreateClick = () => {\n    if (!loadedAtOnce) return;\n\n    if (templates.length >= MAX_TEMPLATE_COUNT) {\n      message.warning(t(\"preset.warning.excceeded\"));\n      return;\n    }\n\n    setCreateDrawerOpen(true);\n  };\n\n  const handleRecordDetailClick = (template: PresetTemplate, index: number) => {\n    setDetailDrawerIndex(index);\n    setDetailDrawerRecord({ ...template });\n    setDetailDrawerOpen(true);\n  };\n\n  const handleRecordDeleteClick = (template: PresetTemplate, index: number) => {\n    modal.confirm({\n      title: <span className=\"text-error\">{t(\"preset.action.delete.modal.title\", { name: template.name })}</span>,\n      content: <span dangerouslySetInnerHTML={{ __html: t(\"preset.action.delete.modal.content\") }} />,\n      icon: (\n        <span className=\"anticon\" role=\"img\">\n          <IconTrash className=\"text-error\" size=\"1em\" />\n        </span>\n      ),\n      okText: t(\"common.button.confirm\"),\n      okButtonProps: { danger: true },\n      onOk: async () => {\n        try {\n          await removeTemplateByIndex(index);\n        } catch (err) {\n          console.error(err);\n          notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n        }\n      },\n    });\n  };\n\n  const handleCreateDrawerSubmit = async (values: PresetTemplate) => {\n    try {\n      await addTemplate(values);\n\n      setCreateDrawerOpen(false);\n    } catch (err) {\n      console.error(err);\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n    }\n  };\n\n  const handleModifyDrawerSubmit = async (values: PresetTemplate) => {\n    try {\n      const newTemplates = [...templates];\n      newTemplates[detailDrawerIndex!] = values;\n      await setTemplates(newTemplates);\n\n      setDetailDrawerIndex(void 0);\n      setDetailDrawerRecord(void 0);\n      setDetailDrawerOpen(false);\n    } catch (err) {\n      console.error(err);\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n    }\n  };\n\n  return (\n    <>\n      <Tips className=\"mb-4\" message={<span dangerouslySetInnerHTML={{ __html: t(\"preset.props.usage.notification.tips\") }}></span>} />\n\n      <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4\">\n        <div className=\"h-40\">\n          <Card\n            className=\"size-full text-gray-500 transition-all select-none hover:text-stone-800 dark:hover:text-stone-200\"\n            styles={{\n              body: {\n                height: \"100%\",\n              },\n            }}\n            hoverable\n            onClick={handleCreateClick}\n          >\n            <div className=\"flex size-full flex-col items-center justify-center gap-4 py-4\">\n              <IconPlus size={36} stroke=\"1.25\" />\n              <div>{t(\"preset.action.create.button\")}</div>\n            </div>\n          </Card>\n        </div>\n\n        {templates.map((template, index) => (\n          <div className=\"h-40\">\n            <Card\n              key={template.name}\n              className=\"size-full\"\n              styles={{\n                root: {\n                  height: \"10rem\",\n                },\n                body: {\n                  height: \"100%\",\n                  padding: \"1rem\",\n                },\n                header: {\n                  padding: \"0.5rem 1rem\",\n                },\n              }}\n              extra={\n                <Dropdown\n                  menu={{\n                    items: [\n                      {\n                        key: \"edit\",\n                        label: t(\"preset.action.modify.menu\"),\n                        icon: (\n                          <span className=\"anticon scale-125\">\n                            <IconEdit size=\"1em\" />\n                          </span>\n                        ),\n                        onClick: (e) => {\n                          e.domEvent.stopPropagation();\n                          handleRecordDetailClick(template, index);\n                        },\n                      },\n                      {\n                        type: \"divider\",\n                      },\n                      {\n                        key: \"delete\",\n                        label: t(\"preset.action.delete.menu\"),\n                        danger: true,\n                        icon: (\n                          <span className=\"anticon scale-125\">\n                            <IconTrash size=\"1em\" />\n                          </span>\n                        ),\n                        onClick: (e) => {\n                          e.domEvent.stopPropagation();\n                          handleRecordDeleteClick(template, index);\n                        },\n                      },\n                    ],\n                  }}\n                  trigger={[\"click\"]}\n                >\n                  <Button\n                    icon={<IconDots size=\"1.25em\" />}\n                    type=\"text\"\n                    onClick={(e) => {\n                      e.stopPropagation();\n                    }}\n                  />\n                </Dropdown>\n              }\n              hoverable\n              title={<Typography.Text ellipsis>{template.name}</Typography.Text>}\n              onClick={() => {\n                handleRecordDetailClick(template, index);\n              }}\n            >\n              <Typography.Text ellipsis type=\"secondary\">\n                {template.subject}\n              </Typography.Text>\n              <Typography.Paragraph ellipsis={{ rows: 2 }} type=\"secondary\">\n                {template.message}\n              </Typography.Paragraph>\n            </Card>\n          </div>\n        ))}\n\n        {loading && !loadedAtOnce && (\n          <div className=\"h-40\">\n            <Card className=\"size-full\" loading size=\"small\" />\n          </div>\n        )}\n      </div>\n\n      <InternalEditDrawer\n        data={{ name: \"\", subject: \"\", message: \"\" }}\n        mode={\"create\"}\n        open={createDrawerOpen}\n        afterClose={() => setCreateDrawerOpen(false)}\n        onOpenChange={(open) => setCreateDrawerOpen(open)}\n        onSubmit={handleCreateDrawerSubmit}\n      />\n      <InternalEditDrawer\n        data={detailDrawerRecord}\n        mode={\"modify\"}\n        open={detailDrawerOpen}\n        afterClose={() => setDetailDrawerOpen(false)}\n        onOpenChange={(open) => setDetailDrawerOpen(open)}\n        onSubmit={handleModifyDrawerSubmit}\n      />\n    </>\n  );\n};\n\nconst InternalEditDrawer = ({\n  mode,\n  data,\n  onSubmit,\n  ...props\n}: {\n  afterClose?: () => void;\n  mode: \"create\" | \"modify\";\n  data?: Nullish<PresetTemplate>;\n  open: boolean;\n  onOpenChange?: (open: boolean) => void;\n  onSubmit?: (record: PresetTemplate) => void;\n}) => {\n  const { t } = useTranslation();\n\n  const { templates } = useNotifyTemplatesStore(useZustandShallowSelector([\"templates\"]));\n\n  const [open, setOpen] = useControllableValue<boolean>(props, {\n    valuePropName: \"open\",\n    defaultValuePropName: \"defaultOpen\",\n    trigger: \"onOpenChange\",\n  });\n\n  const afterClose = () => {\n    formInst.resetFields();\n    props.afterClose?.();\n  };\n\n  const formSchema = z\n    .object({\n      name: z.string().nonempty(t(\"preset.form.name.placeholder\")),\n      subject: z.string().nonempty(t(\"preset.form.notification_subject.placeholder\")),\n      message: z.string().nonempty(t(\"preset.form.notification_message.placeholder\")),\n    })\n    .superRefine((values, ctx) => {\n      if (values.name) {\n        const name = values.name.trim();\n        const duplicatedCount = templates.filter((t) => t.name.trim() === name).length;\n        if (duplicatedCount > (mode === \"create\" ? 0 : 1)) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: t(\"preset.form.name.errmsg.duplicated\"),\n            path: [\"name\"],\n          });\n        }\n      }\n    });\n  const formRule = createSchemaFieldRule(formSchema);\n  const { form: formInst, formProps } = useAntdForm<z.infer<typeof formSchema>>({\n    name: \"viewPresetListNotifyTemplates_InternalDrawerForm_\" + nanoid(),\n    initialValues: data,\n  });\n\n  const handleFormFinish = async (values: z.infer<typeof formSchema>) => {\n    switch (mode) {\n      case \"create\":\n      case \"modify\":\n        {\n          await onSubmit?.(values);\n        }\n        break;\n\n      default:\n        throw \"Invalid props: `mode`\";\n    }\n\n    setOpen(false);\n  };\n\n  return (\n    <DrawerForm\n      {...formProps}\n      clearOnDestroy\n      drawerProps={{ autoFocus: true, destroyOnHidden: true, size: \"large\", afterOpenChange: (open) => !open && afterClose?.() }}\n      form={formInst}\n      layout=\"vertical\"\n      okText={mode === \"create\" ? t(\"common.button.create\") : mode === \"modify\" ? t(\"common.button.save\") : void 0}\n      open={open}\n      preserve={false}\n      title={mode === \"create\" ? t(\"preset.action.create.modal.title\") : mode === \"modify\" ? t(\"preset.action.modify.modal.title\") : void 0}\n      validateTrigger=\"onSubmit\"\n      onFinish={handleFormFinish}\n      onOpenChange={props.onOpenChange}\n    >\n      <Form.Item name=\"name\" label={t(\"preset.form.name.label\")} rules={[formRule]}>\n        <Input maxLength={100} placeholder={t(\"preset.form.name.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name=\"subject\" label={t(\"preset.form.notification_subject.label\")} rules={[formRule]}>\n        <Input placeholder={t(\"preset.form.notification_subject.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name=\"message\" label={t(\"preset.form.notification_message.label\")} rules={[formRule]}>\n        <Input.TextArea autoSize={{ minRows: 10 }} placeholder={t(\"preset.form.notification_message.placeholder\")} />\n      </Form.Item>\n\n      <Divider />\n\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_node.notify.form.template.guide\") }}></span>} />\n      </Form.Item>\n    </DrawerForm>\n  );\n};\n\nexport default PresetListNotifyTemplates;\n"
  },
  {
    "path": "ui/src/pages/presets/PresetListScriptTemplates.tsx",
    "content": "﻿import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { IconDots, IconEdit, IconPlus, IconTrash } from \"@tabler/icons-react\";\nimport { useControllableValue, useMount } from \"ahooks\";\nimport { App, Button, Card, Dropdown, Form, Input, Typography } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { nanoid } from \"nanoid/non-secure\";\nimport { ClientResponseError } from \"pocketbase\";\nimport { z } from \"zod\";\n\nimport CodeTextInput from \"@/components/CodeTextInput\";\nimport DrawerForm from \"@/components/DrawerForm\";\nimport Tips from \"@/components/Tips\";\nimport { useAntdForm, useZustandShallowSelector } from \"@/hooks\";\nimport { useScriptTemplatesStore } from \"@/stores/settings\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nconst MAX_TEMPLATE_COUNT = 99;\n\ntype PresetTemplate = {\n  name: string;\n  command: string;\n};\n\nconst PresetListScriptTemplates = () => {\n  const { t } = useTranslation();\n\n  const { message, modal, notification } = App.useApp();\n\n  const { templates, loading, loadedAtOnce, fetchTemplates, setTemplates, addTemplate, removeTemplateByIndex } = useScriptTemplatesStore();\n  useMount(() => {\n    fetchTemplates().catch((err) => {\n      if (err instanceof ClientResponseError && err.isAbort) {\n        return;\n      }\n\n      console.error(err);\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n    });\n  });\n\n  const [createDrawerOpen, setCreateDrawerOpen] = useState(false);\n  const [detailDrawerOpen, setDetailDrawerOpen] = useState(false);\n  const [detailDrawerRecord, setDetailDrawerRecord] = useState<PresetTemplate>();\n  const [detailDrawerIndex, setDetailDrawerIndex] = useState<number>();\n\n  const handleCreateClick = () => {\n    if (!loadedAtOnce) return;\n\n    if (templates.length >= MAX_TEMPLATE_COUNT) {\n      message.warning(t(\"preset.warning.excceeded\"));\n      return;\n    }\n\n    setCreateDrawerOpen(true);\n  };\n\n  const handleRecordDetailClick = (template: PresetTemplate, index: number) => {\n    setDetailDrawerIndex(index);\n    setDetailDrawerRecord({ ...template });\n    setDetailDrawerOpen(true);\n  };\n\n  const handleRecordDeleteClick = (template: PresetTemplate, index: number) => {\n    modal.confirm({\n      title: <span className=\"text-error\">{t(\"preset.action.delete.modal.title\", { name: template.name })}</span>,\n      content: <span dangerouslySetInnerHTML={{ __html: t(\"preset.action.delete.modal.content\") }} />,\n      icon: (\n        <span className=\"anticon\" role=\"img\">\n          <IconTrash className=\"text-error\" size=\"1em\" />\n        </span>\n      ),\n      okText: t(\"common.button.confirm\"),\n      okButtonProps: { danger: true },\n      onOk: async () => {\n        try {\n          await removeTemplateByIndex(index);\n        } catch (err) {\n          console.error(err);\n          notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n        }\n      },\n    });\n  };\n\n  const handleCreateDrawerSubmit = async (values: PresetTemplate) => {\n    try {\n      await addTemplate(values);\n\n      setCreateDrawerOpen(false);\n    } catch (err) {\n      console.error(err);\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n    }\n  };\n\n  const handleModifyDrawerSubmit = async (values: PresetTemplate) => {\n    try {\n      const newTemplates = [...templates];\n      newTemplates[detailDrawerIndex!] = values;\n      await setTemplates(newTemplates);\n\n      setDetailDrawerIndex(void 0);\n      setDetailDrawerRecord(void 0);\n      setDetailDrawerOpen(false);\n    } catch (err) {\n      console.error(err);\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n    }\n  };\n\n  return (\n    <>\n      <Tips className=\"mb-4\" message={<span dangerouslySetInnerHTML={{ __html: t(\"preset.props.usage.script.tips\") }}></span>} />\n\n      <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4\">\n        <div className=\"h-40\">\n          <Card\n            className=\"size-full text-gray-500 transition-all select-none hover:text-stone-800 dark:hover:text-stone-200\"\n            styles={{\n              root: {\n                height: \"100%\",\n              },\n              body: {\n                height: \"100%\",\n              },\n            }}\n            hoverable\n            onClick={handleCreateClick}\n          >\n            <div className=\"flex size-full flex-col items-center justify-center gap-4 py-4\">\n              <IconPlus size={36} stroke=\"1.25\" />\n              <div>{t(\"preset.action.create.button\")}</div>\n            </div>\n          </Card>\n        </div>\n\n        {templates.map((template, index) => (\n          <div className=\"h-40\">\n            <Card\n              key={template.name}\n              className=\"size-full\"\n              styles={{\n                body: {\n                  height: \"100%\",\n                  padding: \"1rem\",\n                },\n                header: {\n                  padding: \"0.5rem 1rem\",\n                },\n              }}\n              extra={\n                <Dropdown\n                  menu={{\n                    items: [\n                      {\n                        key: \"edit\",\n                        label: t(\"preset.action.modify.menu\"),\n                        icon: (\n                          <span className=\"anticon scale-125\">\n                            <IconEdit size=\"1em\" />\n                          </span>\n                        ),\n                        onClick: (e) => {\n                          e.domEvent.stopPropagation();\n                          handleRecordDetailClick(template, index);\n                        },\n                      },\n                      {\n                        type: \"divider\",\n                      },\n                      {\n                        key: \"delete\",\n                        label: t(\"preset.action.delete.menu\"),\n                        danger: true,\n                        icon: (\n                          <span className=\"anticon scale-125\">\n                            <IconTrash size=\"1em\" />\n                          </span>\n                        ),\n                        onClick: (e) => {\n                          e.domEvent.stopPropagation();\n                          handleRecordDeleteClick(template, index);\n                        },\n                      },\n                    ],\n                  }}\n                  trigger={[\"click\"]}\n                >\n                  <Button\n                    icon={<IconDots size=\"1.25em\" />}\n                    type=\"text\"\n                    onClick={(e) => {\n                      e.stopPropagation();\n                    }}\n                  />\n                </Dropdown>\n              }\n              hoverable\n              title={<Typography.Text ellipsis>{template.name}</Typography.Text>}\n              onClick={() => {\n                handleRecordDetailClick(template, index);\n              }}\n            >\n              <Typography.Paragraph className=\"whitespace-pre-line\" ellipsis={{ rows: 3 }} type=\"secondary\">\n                {template.command}\n              </Typography.Paragraph>\n            </Card>\n          </div>\n        ))}\n\n        {loading && !loadedAtOnce && (\n          <div className=\"h-40\">\n            <Card className=\"size-full\" loading size=\"small\" />\n          </div>\n        )}\n      </div>\n\n      <InternalEditDrawer\n        data={{ name: \"\", command: \"\" }}\n        mode={\"create\"}\n        open={createDrawerOpen}\n        afterClose={() => setCreateDrawerOpen(false)}\n        onOpenChange={(open) => setCreateDrawerOpen(open)}\n        onSubmit={handleCreateDrawerSubmit}\n      />\n      <InternalEditDrawer\n        data={detailDrawerRecord}\n        mode={\"modify\"}\n        open={detailDrawerOpen}\n        afterClose={() => setDetailDrawerOpen(false)}\n        onOpenChange={(open) => setDetailDrawerOpen(open)}\n        onSubmit={handleModifyDrawerSubmit}\n      />\n    </>\n  );\n};\n\nconst InternalEditDrawer = ({\n  mode,\n  data,\n  onSubmit,\n  ...props\n}: {\n  afterClose?: () => void;\n  mode: \"create\" | \"modify\";\n  data?: Nullish<PresetTemplate>;\n  open: boolean;\n  onOpenChange?: (open: boolean) => void;\n  onSubmit?: (record: PresetTemplate) => void;\n}) => {\n  const { t } = useTranslation();\n\n  const { templates } = useScriptTemplatesStore(useZustandShallowSelector([\"templates\"]));\n\n  const [open, setOpen] = useControllableValue<boolean>(props, {\n    valuePropName: \"open\",\n    defaultValuePropName: \"defaultOpen\",\n    trigger: \"onOpenChange\",\n  });\n\n  const afterClose = () => {\n    formInst.resetFields();\n    props.afterClose?.();\n  };\n\n  const formSchema = z\n    .object({\n      name: z.string().nonempty(t(\"preset.form.name.placeholder\")),\n      command: z.string().nonempty(t(\"preset.form.script_command.placeholder\")),\n    })\n    .superRefine((values, ctx) => {\n      if (values.name) {\n        const name = values.name.trim();\n        const duplicatedCount = templates.filter((t) => t.name.trim() === name).length;\n        if (duplicatedCount > (mode === \"create\" ? 0 : 1)) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: t(\"preset.form.name.errmsg.duplicated\"),\n            path: [\"name\"],\n          });\n        }\n      }\n    });\n  const formRule = createSchemaFieldRule(formSchema);\n  const { form: formInst, formProps } = useAntdForm<z.infer<typeof formSchema>>({\n    name: \"viewPresetListScriptTemplates_InternalDrawerForm_\" + nanoid(),\n    initialValues: data,\n  });\n\n  const handleFormFinish = async (values: z.infer<typeof formSchema>) => {\n    switch (mode) {\n      case \"create\":\n      case \"modify\":\n        {\n          await onSubmit?.(values);\n        }\n        break;\n\n      default:\n        throw \"Invalid props: `mode`\";\n    }\n\n    setOpen(false);\n  };\n\n  return (\n    <DrawerForm\n      {...formProps}\n      clearOnDestroy\n      drawerProps={{ autoFocus: true, destroyOnHidden: true, size: \"large\", afterOpenChange: (open) => !open && afterClose?.() }}\n      form={formInst}\n      layout=\"vertical\"\n      okText={mode === \"create\" ? t(\"common.button.create\") : mode === \"modify\" ? t(\"common.button.save\") : void 0}\n      open={open}\n      preserve={false}\n      title={mode === \"create\" ? t(\"preset.action.create.modal.title\") : mode === \"modify\" ? t(\"preset.action.modify.modal.title\") : void 0}\n      validateTrigger=\"onSubmit\"\n      onFinish={handleFormFinish}\n      onOpenChange={props.onOpenChange}\n    >\n      <Form.Item name=\"name\" label={t(\"preset.form.name.label\")} rules={[formRule]}>\n        <Input maxLength={100} placeholder={t(\"preset.form.name.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name=\"command\" label={t(\"preset.form.script_command.label\")} rules={[formRule]}>\n        <CodeTextInput height=\"auto\" minHeight=\"256px\" language={[\"shell\", \"powershell\"]} placeholder={t(\"preset.form.script_command.placeholder\")} />\n      </Form.Item>\n    </DrawerForm>\n  );\n};\n\nexport default PresetListScriptTemplates;\n"
  },
  {
    "path": "ui/src/pages/settings/Settings.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Outlet, useLocation, useNavigate } from \"react-router-dom\";\nimport { IconDatabaseCog, IconHeartRateMonitor, IconInfoCircle, IconPalette, IconPlugConnected, IconUserShield } from \"@tabler/icons-react\";\nimport { Menu } from \"antd\";\n\nconst Settings = () => {\n  const location = useLocation();\n  const navigate = useNavigate();\n\n  const { t } = useTranslation();\n\n  const menus = [\n    [\"account\", \"settings.account.tab\", <IconUserShield size=\"1em\" />],\n    [\"appearance\", \"settings.appearance.tab\", <IconPalette size=\"1em\" />],\n    [\"ssl-provider\", \"settings.sslprovider.tab\", <IconPlugConnected size=\"1em\" />],\n    [\"persistence\", \"settings.persistence.tab\", <IconDatabaseCog size=\"1em\" />],\n    [\"diagnostics\", \"settings.diagnostics.tab\", <IconHeartRateMonitor size=\"1em\" />],\n    [\"about\", \"settings.about.tab\", <IconInfoCircle size=\"1em\" />],\n  ] satisfies [string, string, React.ReactElement][];\n  const [menuKey, setMenuKey] = useState<string>(() => location.pathname.split(\"/\")[2]);\n  useEffect(() => {\n    const subpath = location.pathname.split(\"/\")[2];\n    if (!subpath) {\n      navigate(\"/settings/account\");\n      return;\n    }\n\n    setMenuKey(subpath);\n  }, [location.pathname]);\n\n  const handleMenuClick = ({ key }: { key: string }) => {\n    setMenuKey(key);\n    navigate(`/settings/${key}`);\n  };\n\n  return (\n    <div className=\"px-6 py-4\">\n      <div className=\"container\">\n        <h1>{t(\"settings.page.title\")}</h1>\n      </div>\n\n      <div className=\"container\">\n        <div className=\"hidden select-none max-lg:block\">\n          <Menu\n            style={{ background: \"transparent\", borderInlineEnd: \"none\" }}\n            mode=\"horizontal\"\n            selectedKeys={[menuKey]}\n            items={menus.map(([key, label, icon]) => ({\n              key,\n              label: t(label),\n              icon: (\n                <span className=\"anticon scale-125\" role=\"img\">\n                  {icon}\n                </span>\n              ),\n            }))}\n            onClick={handleMenuClick}\n          />\n        </div>\n\n        <div className=\"flex h-full justify-stretch gap-x-4 overflow-hidden\">\n          <div className=\"w-[256px] select-none max-lg:hidden\">\n            <Menu\n              style={{ background: \"transparent\", borderInlineEnd: \"none\" }}\n              mode=\"vertical\"\n              selectedKeys={[menuKey]}\n              items={menus.map(([key, label, icon]) => ({\n                key,\n                label: t(label),\n                icon: (\n                  <span className=\"anticon scale-125\" role=\"img\">\n                    {icon}\n                  </span>\n                ),\n              }))}\n              onClick={handleMenuClick}\n            />\n          </div>\n\n          <div className=\"w-full flex-1\">\n            <div className=\"px-4 max-lg:px-0 max-lg:py-6\">\n              <Outlet />\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default Settings;\n"
  },
  {
    "path": "ui/src/pages/settings/SettingsAbout.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { IconBook, IconBrandGithub, IconBrandTelegram, IconCoin, IconMessageChatbot } from \"@tabler/icons-react\";\nimport { Badge, Button, Divider, List, Tooltip, Typography } from \"antd\";\n\nimport { APP_DOCUMENT_URL, APP_DOWNLOAD_URL, APP_REPO_URL, APP_VERSION } from \"@/domain/app\";\nimport { useVersionChecker } from \"@/hooks\";\n\nconst SettingsAbout = () => {\n  const { t } = useTranslation();\n\n  const { hasUpdate } = useVersionChecker();\n\n  const handleDownloadClick = () => {\n    window.open(APP_DOWNLOAD_URL, \"_blank\");\n  };\n\n  const handleDocumentClick = () => {\n    window.open(APP_DOCUMENT_URL, \"_blank\");\n  };\n\n  const handleGithubClick = () => {\n    window.open(APP_REPO_URL, \"_blank\");\n  };\n\n  const handleTelegramClick = () => {\n    window.open(\"https://t.me/+ZXphsppxUg41YmVl\", \"_blank\");\n  };\n\n  const handleDonateClick = () => {\n    window.open(\"https://profile.ikit.fun/sponsors/\", \"_blank\");\n  };\n\n  const handleFeedbackClick = () => {\n    window.open(APP_REPO_URL + \"/issues\", \"_blank\");\n  };\n\n  return (\n    <>\n      <h2>Certimate</h2>\n      <div className=\"mb-4\">\n        <div className=\"flex items-center gap-2\">\n          <Typography.Text type=\"secondary\">Version: {APP_VERSION}</Typography.Text>\n          <Badge className=\"cursor-pointer\" count={hasUpdate ? t(\"settings.about.version.new\") : void 0} onClick={handleDownloadClick} />\n        </div>\n      </div>\n      <div className=\"mb-2 flex flex-wrap items-center gap-2\">\n        <Tooltip title={t(\"settings.about.socials.document\")}>\n          <Button type=\"text\" icon={<IconBook size=\"1.5em\" onClick={handleDocumentClick} />} />\n        </Tooltip>\n        <Tooltip title={t(\"settings.about.socials.github\")}>\n          <Button type=\"text\" icon={<IconBrandGithub size=\"1.5em\" onClick={handleGithubClick} />} />\n        </Tooltip>\n        <Tooltip title={t(\"settings.about.socials.telegram\")}>\n          <Button type=\"text\" icon={<IconBrandTelegram size=\"1.5em\" onClick={handleTelegramClick} />} />\n        </Tooltip>\n        <Tooltip title={t(\"settings.about.socials.donate\")}>\n          <Button type=\"text\" icon={<IconCoin size=\"1.5em\" onClick={handleDonateClick} />} />\n        </Tooltip>\n      </div>\n\n      <Divider />\n\n      <h2>{t(\"settings.about.contributors.title\")}</h2>\n      <div className=\"mb-4\">\n        <Typography.Text type=\"secondary\">{t(\"settings.about.contributors.tips\")}</Typography.Text>\n      </div>\n      <div className=\"mb-2 md:max-w-160\">\n        <img className=\"max-w-full\" src=\"https://contrib.rocks/image?repo=certimate-go/certimate\" alt=\"Contributors\" />\n      </div>\n\n      <Divider />\n\n      <div className=\"md:max-w-160\">\n        <List bordered>\n          <List.Item extra={<Button onClick={handleFeedbackClick}>{t(\"settings.about.feedback.button\")}</Button>}>\n            <List.Item.Meta\n              avatar={<IconMessageChatbot size=\"1.5em\" />}\n              title={t(\"settings.about.feedback.title\")}\n              description={t(\"settings.about.feedback.subtitle\")}\n            />\n          </List.Item>\n        </List>\n      </div>\n    </>\n  );\n};\n\nexport default SettingsAbout;\n"
  },
  {
    "path": "ui/src/pages/settings/SettingsAccount.tsx",
    "content": "import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useNavigate } from \"react-router-dom\";\nimport { App, Button, Divider, Flex, Form, Input, Typography } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { z } from \"zod\";\n\nimport { useAntdForm } from \"@/hooks\";\nimport { authWithPassword, getAuthStore, save as saveAdmin } from \"@/repository/admin\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nconst SettingsAccount = () => {\n  const { t } = useTranslation();\n\n  return (\n    <>\n      <h2>{t(\"settings.account.username.title\")}</h2>\n      <SettingsAccountUsername className=\"md:max-w-160\" />\n\n      <Divider />\n\n      <h2>{t(\"settings.account.password.title\")}</h2>\n      <SettingsAccountPassword className=\"md:max-w-160\" />\n\n      {/* <Divider />\n\n      <h2>{t(\"settings.account.2fa.title\")}</h2>\n      <div>TODO ...</div> */}\n    </>\n  );\n};\n\nconst SettingsAccountUsername = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {\n  const navigate = useNavigate();\n\n  const { t } = useTranslation();\n\n  const { message, notification } = App.useApp();\n\n  const formSchema = z.object({\n    username: z.email(t(\"common.errmsg.email_invalid\")).max(256, t(\"common.errmsg.string_max\", { max: 256 })),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const {\n    form: formInst,\n    formPending,\n    formProps,\n  } = useAntdForm<z.infer<typeof formSchema>>({\n    initialValues: {\n      username: getAuthStore().record?.email,\n    },\n    onSubmit: async (values) => {\n      try {\n        await saveAdmin({ email: values.username });\n\n        message.success(t(\"common.text.operation_succeeded\"));\n\n        setTimeout(() => {\n          getAuthStore().clear();\n          navigate(\"/login\");\n        }, 500);\n      } catch (err) {\n        notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n\n        throw err;\n      }\n    },\n  });\n  const [formVisible, setFormVisible] = useState(false);\n  const [formChanged, setFormChanged] = useState(false);\n\n  const handleInputChange = () => {\n    setFormChanged(formInst.getFieldValue(\"username\") !== formProps.initialValues?.username);\n  };\n\n  const handleEditClick = () => {\n    setFormVisible(true);\n    formInst.resetFields();\n  };\n\n  const handleCancelClick = () => {\n    setFormVisible(false);\n    setFormChanged(false);\n  };\n\n  return (\n    <div className={className} style={style}>\n      <Form {...formProps} form={formInst} disabled={formPending} layout=\"vertical\">\n        {formVisible ? (\n          <>\n            <Form.Item name=\"username\" label={t(\"settings.account.username.form.email.label\")} rules={[formRule]}>\n              <Input autoFocus placeholder={t(\"settings.account.username.form.email.placeholder\")} onChange={handleInputChange} />\n            </Form.Item>\n\n            <Form.Item>\n              <Flex align=\"center\" gap=\"small\">\n                <Button type=\"primary\" htmlType=\"submit\" disabled={!formChanged} loading={formPending}>\n                  {t(\"common.button.save\")}\n                </Button>\n                <Button disabled={formPending} onClick={handleCancelClick}>\n                  {t(\"common.button.cancel\")}\n                </Button>\n              </Flex>\n            </Form.Item>\n          </>\n        ) : (\n          <>\n            <div className=\"mb-2\">\n              <Typography.Text type=\"secondary\">{t(\"settings.account.username.tips\")}</Typography.Text>\n            </div>\n            <div className=\"mb-2\">\n              <Typography.Text>{getAuthStore().record?.email}</Typography.Text>\n            </div>\n\n            <Button onClick={handleEditClick}>{t(\"settings.account.username.button.label\")}</Button>\n          </>\n        )}\n      </Form>\n    </div>\n  );\n};\n\nconst SettingsAccountPassword = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {\n  const navigate = useNavigate();\n\n  const { t } = useTranslation();\n\n  const { message, notification } = App.useApp();\n\n  const formSchema = z.object({\n    oldPassword: z\n      .string()\n      .min(10, t(\"settings.account.password.form.email.password.errmsg.invalid\"))\n      .max(256, t(\"common.errmsg.string_max\", { max: 256 })),\n    newPassword: z\n      .string()\n      .min(10, t(\"settings.account.password.form.email.password.errmsg.invalid\"))\n      .max(256, t(\"common.errmsg.string_max\", { max: 256 })),\n    confirmPassword: z.string().refine((v) => v === formInst.getFieldValue(\"newPassword\"), {\n      error: t(\"settings.account.password.form.email.password.errmsg.not_matched\"),\n    }),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const {\n    form: formInst,\n    formPending,\n    formProps,\n  } = useAntdForm({\n    initialValues: {\n      oldPassword: \"\",\n      newPassword: \"\",\n      confirmPassword: \"\",\n    },\n    onSubmit: async (values) => {\n      try {\n        await authWithPassword(getAuthStore().record!.email, values.oldPassword);\n        await saveAdmin({ password: values.newPassword, passwordConfirm: values.confirmPassword });\n\n        message.success(t(\"common.text.operation_succeeded\"));\n\n        setTimeout(() => {\n          getAuthStore().clear();\n          navigate(\"/login\");\n        }, 500);\n      } catch (err) {\n        notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n\n        throw err;\n      }\n    },\n  });\n  const [formVisible, setFormVisible] = useState(false);\n  const [formChanged, setFormChanged] = useState(false);\n\n  const handleInputChange = () => {\n    const values = formInst.getFieldsValue();\n    setFormChanged(!!values.oldPassword && !!values.newPassword && !!values.confirmPassword);\n  };\n\n  const handleEditClick = () => {\n    setFormVisible(true);\n    formInst.resetFields();\n  };\n\n  const handleCancelClick = () => {\n    setFormVisible(false);\n    setFormChanged(false);\n  };\n\n  return (\n    <div className={className} style={style}>\n      <Form {...formProps} form={formInst} disabled={formPending} layout=\"vertical\">\n        {formVisible ? (\n          <>\n            <Form.Item name=\"oldPassword\" label={t(\"settings.account.password.form.email.old_password.label\")} rules={[formRule]}>\n              <Input.Password autoFocus placeholder={t(\"settings.account.password.form.email.old_password.placeholder\")} onChange={handleInputChange} />\n            </Form.Item>\n\n            <Form.Item name=\"newPassword\" label={t(\"settings.account.password.form.email.new_password.label\")} rules={[formRule]}>\n              <Input.Password placeholder={t(\"settings.account.password.form.email.new_password.placeholder\")} onChange={handleInputChange} />\n            </Form.Item>\n\n            <Form.Item name=\"confirmPassword\" label={t(\"settings.account.password.form.email.confirm_password.label\")} rules={[formRule]}>\n              <Input.Password placeholder={t(\"settings.account.password.form.email.confirm_password.placeholder\")} onChange={handleInputChange} />\n            </Form.Item>\n\n            <Form.Item>\n              <Flex align=\"center\" gap=\"small\">\n                <Button type=\"primary\" htmlType=\"submit\" disabled={!formChanged} loading={formPending}>\n                  {t(\"common.button.save\")}\n                </Button>\n                <Button disabled={formPending} onClick={handleCancelClick}>\n                  {t(\"common.button.cancel\")}\n                </Button>\n              </Flex>\n            </Form.Item>\n          </>\n        ) : (\n          <>\n            <div className=\"mb-2\">\n              <Typography.Text type=\"secondary\">{t(\"settings.account.password.tips\")}</Typography.Text>\n            </div>\n\n            <Button onClick={handleEditClick}>{t(\"settings.account.password.button.label\")}</Button>\n          </>\n        )}\n      </Form>\n    </div>\n  );\n};\n\nexport default SettingsAccount;\n"
  },
  {
    "path": "ui/src/pages/settings/SettingsAppearance.tsx",
    "content": "import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Divider, Form, Radio, type RadioChangeEvent, Select, theme } from \"antd\";\nimport { produce } from \"immer\";\n\nimport { useAppLocaleMenuItems } from \"@/components/AppLocale\";\nimport { useAppThemeMenuItems } from \"@/components/AppTheme\";\nimport { useAppSettings, useBrowserTheme } from \"@/hooks\";\n\nconst SettingsAppearance = () => {\n  const { t } = useTranslation();\n\n  return (\n    <>\n      <h2>{t(\"settings.appearance.theme.title\")}</h2>\n      <SettingsAppearanceTheme className=\"md:max-w-160\" />\n\n      <Divider />\n\n      <h2>{t(\"settings.appearance.language.title\")}</h2>\n      <SettingsAppearanceLanguage className=\"md:max-w-160\" />\n\n      <Divider />\n\n      <h2>{t(\"settings.appearance.pagination.title\")}</h2>\n      <SettingsAppearancePagination className=\"md:max-w-160\" />\n\n      <Divider />\n\n      <h2>{t(\"settings.appearance.workflow.title\")}</h2>\n      <SettingsAppearanceWorkflow className=\"md:max-w-160\" />\n    </>\n  );\n};\n\nconst SettingsAppearanceTheme = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {\n  const { t } = useTranslation();\n\n  const { token: themeToken } = theme.useToken();\n\n  const { themeMode, setThemeMode } = useBrowserTheme();\n  const themeItems = useAppThemeMenuItems();\n  const [themeChanged, setThemeChanged] = useState(false);\n\n  const handleChange = (e: RadioChangeEvent) => {\n    if (e.target.value !== themeMode) {\n      setThemeChanged(true);\n      setThemeMode(e.target.value);\n    }\n  };\n\n  return (\n    <div className={className} style={style}>\n      <Form layout=\"vertical\">\n        <Form.Item extra={themeChanged ? t(\"settings.appearance.theme.form.value.extra\") : void 0}>\n          <Radio.Group block value={themeMode} onChange={handleChange}>\n            <div className=\"flex w-full items-center gap-4 max-md:flex-wrap\">\n              {themeItems.map((item) => (\n                <div className=\"relative max-w-44 flex-1/3 max-md:flex-1/2 max-sm:flex-1\" key={item.key}>\n                  <div className=\"overflow-hidden rounded-lg border border-solid\" style={{ borderColor: themeToken.colorBorder }}>\n                    <img className=\"mb-2 w-full\" src={`/imgs/themes/${item.key}.png`} />\n                    <div className=\"mb-2 px-2\">\n                      <Radio value={item.key}>{item.label}</Radio>\n                    </div>\n                  </div>\n                </div>\n              ))}\n            </div>\n          </Radio.Group>\n        </Form.Item>\n      </Form>\n    </div>\n  );\n};\n\nconst SettingsAppearanceLanguage = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {\n  const { i18n, t } = useTranslation();\n\n  const localeItems = useAppLocaleMenuItems();\n  const [localeChanged, setLocaleChanged] = useState(false);\n\n  const handleChange = (value: string) => {\n    if (value !== (i18n.resolvedLanguage ?? i18n.language)) {\n      setLocaleChanged(true);\n      i18n.changeLanguage(value);\n    }\n  };\n\n  return (\n    <div className={className} style={style}>\n      <Form layout=\"vertical\">\n        <Form.Item extra={localeChanged ? t(\"settings.appearance.language.form.value.extra\") : void 0}>\n          <Select\n            options={localeItems.map((item) => ({\n              key: item.key,\n              value: item.key,\n              label: item.label,\n            }))}\n            value={i18n.resolvedLanguage ?? i18n.language}\n            onChange={handleChange}\n          />\n        </Form.Item>\n      </Form>\n    </div>\n  );\n};\n\nconst SettingsAppearancePagination = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {\n  const { t } = useTranslation();\n\n  const { appSettings: globalAppSettings, setAppSettings: setGlobalAppSettings } = useAppSettings();\n\n  const handleChange = (value: (typeof globalAppSettings)[\"defaultPerPage\"]) => {\n    setGlobalAppSettings(\n      produce(globalAppSettings, (draft) => {\n        draft.defaultPerPage = value;\n      })\n    );\n  };\n\n  return (\n    <div className={className} style={style}>\n      <Form layout=\"vertical\">\n        <Form.Item label={t(\"settings.appearance.pagination.form.default_per_page.label\")}>\n          <Select\n            options={[10, 15, 20, 30, 50, 100].map((value) => ({\n              key: value,\n              value: value,\n              label: `${value} ${t(\"settings.appearance.pagination.form.default_per_page.unit\")}`,\n            }))}\n            placeholder={t(\"settings.appearance.pagination.form.default_per_page.placeholder\")}\n            defaultValue={globalAppSettings.defaultPerPage}\n            onChange={handleChange}\n          />\n        </Form.Item>\n      </Form>\n    </div>\n  );\n};\n\nconst SettingsAppearanceWorkflow = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {\n  const { t } = useTranslation();\n\n  const { appSettings: globalAppSettings, setAppSettings: setGlobalAppSettings } = useAppSettings();\n\n  const handleChange = (value: (typeof globalAppSettings)[\"defaultWorkflowLayout\"]) => {\n    setGlobalAppSettings(\n      produce(globalAppSettings, (draft) => {\n        draft.defaultWorkflowLayout = value;\n      })\n    );\n  };\n\n  return (\n    <div className={className} style={style}>\n      <Form layout=\"vertical\">\n        <Form.Item label={t(\"settings.appearance.workflow.form.default_designer_layout.label\")}>\n          <Select\n            options={[\"horizontal\", \"vertical\"].map((value) => ({\n              key: value,\n              value: value,\n              label: t(`settings.appearance.workflow.form.default_designer_layout.option.${value}`),\n            }))}\n            placeholder={t(\"settings.appearance.workflow.form.default_designer_layout.placeholder\")}\n            defaultValue={globalAppSettings.defaultWorkflowLayout}\n            onChange={handleChange}\n          />\n        </Form.Item>\n      </Form>\n    </div>\n  );\n};\n\nexport default SettingsAppearance;\n"
  },
  {
    "path": "ui/src/pages/settings/SettingsDiagnostics.tsx",
    "content": "import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { IconReload } from \"@tabler/icons-react\";\nimport { useRequest } from \"ahooks\";\nimport { Button, Card, Divider, Empty, List, Pagination, Statistic, Tag, Tooltip, Typography } from \"antd\";\nimport dayjs from \"dayjs\";\n\nimport { getStats as getWorkflowStats } from \"@/api/workflows\";\nimport Show from \"@/components/Show\";\nimport { listCronJobs, listLogs } from \"@/repository/system\";\nimport { getNextCronExecutions } from \"@/utils/cron\";\nimport { mergeCls } from \"@/utils/css\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nconst SettingsDiagnostics = () => {\n  const { t } = useTranslation();\n\n  return (\n    <>\n      <h2>{t(\"settings.diagnostics.logs.title\")}</h2>\n      <SettingsDiagnosticsLogs />\n\n      <Divider />\n\n      <h2>{t(\"settings.diagnostics.crons.title\")}</h2>\n      <SettingsDiagnosticsCrons />\n\n      <Divider />\n\n      <h2>{t(\"settings.diagnostics.workflow_dispatcher.title\")}</h2>\n      <SettingsDiagnosticsWorkflowDispatcher />\n    </>\n  );\n};\n\nconst SettingsDiagnosticsLogs = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {\n  const { t } = useTranslation();\n\n  const [page, setPage] = useState(1);\n  const [pageSize] = useState(10);\n\n  type Log = Awaited<ReturnType<typeof listLogs>>[\"items\"][number];\n  const [listData, setListData] = useState<Log[]>([]);\n\n  const [hasMore, setHasMore] = useState(true);\n\n  const {\n    loading,\n    error: loadError,\n    run: refreshData,\n  } = useRequest(\n    () => {\n      return listLogs({ page: page, perPage: pageSize });\n    },\n    {\n      refreshDeps: [page, pageSize],\n      debounceWait: 300,\n      debounceLeading: true,\n      onSuccess: (res) => {\n        if (page === 1) {\n          setListData([]);\n        }\n\n        setListData((prev) => [...prev, ...res.items]);\n        setHasMore(res.items.length >= pageSize);\n      },\n    }\n  );\n\n  const renderLogRecord = (record: Log) => {\n    let message = <>{record.message}</>;\n    if (record.data != null && Object.keys(record.data).length > 0) {\n      message = (\n        <details>\n          <summary>{record.message}</summary>\n          {Object.entries(record.data).map(([key, value]) => (\n            <div key={key} className=\"flex space-x-2\" style={{ wordBreak: \"break-word\" }}>\n              <div>{key}:</div>\n              <div>{JSON.stringify(value)}</div>\n            </div>\n          ))}\n        </details>\n      );\n    }\n\n    enum LogLevel {\n      Info = 0,\n      Warn = 4,\n      Error = 8,\n    }\n\n    return (\n      <div className=\"flex space-x-2\">\n        <div className=\"font-mono whitespace-nowrap text-stone-400\">[{dayjs(record.created).format(\"YYYY-MM-DD HH:mm:ss\")}]</div>\n        <div\n          className={mergeCls(\n            \"flex-1 font-mono\",\n            +record.level < LogLevel.Info\n              ? \"text-stone-400\"\n              : +record.level < LogLevel.Warn\n                ? \"\"\n                : +record.level < LogLevel.Error\n                  ? \"text-warning\"\n                  : \"text-error\"\n          )}\n        >\n          {message}\n        </div>\n      </div>\n    );\n  };\n\n  const handleReloadClick = () => {\n    refreshData();\n  };\n\n  const handleRefreshClick = () => {\n    setPage(1);\n    refreshData();\n  };\n\n  const handleLoadMoreClick = () => {\n    setPage((prev) => prev + 1);\n  };\n\n  return (\n    <div className={className} style={style}>\n      <div className=\"size-full overflow-hidden rounded-md bg-black text-stone-200\">\n        <div className=\"relative\">\n          <Show>\n            <Show.Case when={loading}>\n              <div className=\"absolute top-4 right-8\">\n                <Button className=\"pointer-none\" loading>\n                  Loading ...\n                </Button>\n              </div>\n            </Show.Case>\n\n            <Show.Case when={listData.length === 0}>\n              <div className=\"px-4 py-2\">\n                <div className=\"w-full overflow-hidden\">\n                  <div className=\"text-xs/relaxed text-stone-400\">{loadError ? `> ${unwrapErrMsg(loadError)}` : \"> no logs avaiable\"}</div>\n                </div>\n                <div className=\"flex w-full items-center\">\n                  <a onClick={handleReloadClick}>\n                    <span className=\"text-xs\">{t(\"common.button.reload\")}</span>\n                  </a>\n                </div>\n              </div>\n            </Show.Case>\n\n            <Show.Default>\n              <div className=\"px-4 py-2\">\n                <div className=\"flex w-full flex-col overflow-hidden\">\n                  {listData.map((record) => {\n                    return (\n                      <div key={record.id} className=\"text-xs/relaxed\">\n                        {renderLogRecord(record)}\n                      </div>\n                    );\n                  })}\n                </div>\n                <div className=\"flex w-full items-center\">\n                  <a onClick={handleRefreshClick}>\n                    <span className=\"text-xs\">{t(\"settings.diagnostics.logs.button.refresh.label\")}</span>\n                  </a>\n                  {hasMore && (\n                    <>\n                      <Divider orientation=\"vertical\" />\n                      <a onClick={handleLoadMoreClick}>\n                        <span className=\"text-xs\">{t(\"settings.diagnostics.logs.button.load_more.label\")}</span>\n                      </a>\n                    </>\n                  )}\n                </div>\n              </div>\n            </Show.Default>\n          </Show>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst SettingsDiagnosticsCrons = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {\n  const { t } = useTranslation();\n\n  const [page, setPage] = useState(1);\n  const [pageSize, setPageSize] = useState(10);\n\n  type CronJob = Awaited<ReturnType<typeof listCronJobs>>[\"items\"][number] & {\n    nextTriggerTime: string;\n  };\n  const [listData, setListData] = useState<CronJob[]>([]);\n  const [listTotal, setListTotal] = useState(0);\n\n  const {\n    loading,\n    error: loadError,\n    run: refreshData,\n  } = useRequest(\n    () => {\n      return listCronJobs().then((res) => {\n        const startIndex = (page - 1) * pageSize;\n        const endIndex = startIndex + pageSize;\n        return {\n          items: res.items\n            .slice(startIndex, endIndex)\n            .map((item) => ({\n              ...item,\n              nextTriggerTime: dayjs(getNextCronExecutions(item.cron)[0]).format(\"YYYY-MM-DD HH:mm:ss\"),\n            }))\n            .sort((a, b) => a.nextTriggerTime.localeCompare(b.nextTriggerTime)),\n          totalItems: res.items.length,\n        };\n      });\n    },\n    {\n      refreshDeps: [page, pageSize],\n      onSuccess: (res) => {\n        setListData(res.items);\n        setListTotal(res.totalItems);\n      },\n    }\n  );\n\n  const handleReloadClick = () => {\n    refreshData();\n  };\n\n  const handlePaginationChange = (page: number, pageSize: number) => {\n    setPage(page);\n    setPageSize(pageSize);\n  };\n\n  return (\n    <div className={className} style={style}>\n      <List<CronJob>\n        bordered\n        dataSource={listData}\n        loading={loading}\n        locale={{\n          emptyText: (\n            <Empty description={loadError ? unwrapErrMsg(loadError) : t(\"common.text.nodata\")} image={Empty.PRESENTED_IMAGE_SIMPLE}>\n              {loadError && (\n                <Button ghost icon={<IconReload size=\"1.25em\" />} type=\"primary\" onClick={handleReloadClick}>\n                  {t(\"common.button.reload\")}\n                </Button>\n              )}\n            </Empty>\n          ),\n        }}\n        rowKey={(record) => record.id}\n        renderItem={(record) => (\n          <List.Item>\n            <Tooltip\n              title={\n                <>\n                  {t(\"settings.diagnostics.crons.props.next_trigger_time\")}\n                  <br />\n                  {record.nextTriggerTime}\n                </>\n              }\n              mouseEnterDelay={1}\n              placement=\"topRight\"\n            >\n              <div className=\"flex w-full items-center justify-between gap-4 overflow-hidden xl:hidden\">\n                <div className=\"flex-1 truncate\">\n                  <Typography.Text>{record.id}</Typography.Text>\n                </div>\n                <div className=\"text-right\">\n                  <Typography.Text type=\"secondary\">{record.cron}</Typography.Text>\n                </div>\n              </div>\n            </Tooltip>\n\n            <div className=\"hidden w-full items-center justify-between gap-4 overflow-hidden xl:flex\">\n              <div className=\"flex-1 truncate\">\n                <Typography.Text>{record.id}</Typography.Text>\n              </div>\n              <div className=\"flex items-center justify-end\">\n                <Typography.Text type=\"secondary\">{record.cron}</Typography.Text>\n                <Divider orientation=\"vertical\" />\n                <Typography.Text type=\"secondary\">\n                  {t(\"settings.diagnostics.crons.props.next_trigger_time\")}\n                  {record.nextTriggerTime}\n                </Typography.Text>\n              </div>\n            </div>\n          </List.Item>\n        )}\n      />\n      <Show when={page > 1 || listTotal > pageSize}>\n        <div className=\"mt-4 flex justify-end\">\n          <Pagination current={page} pageSize={pageSize} size=\"small\" total={listTotal} onChange={handlePaginationChange} />\n        </div>\n      </Show>\n    </div>\n  );\n};\n\nconst SettingsDiagnosticsWorkflowDispatcher = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {\n  const { t } = useTranslation();\n\n  type Statistics = Awaited<ReturnType<typeof getWorkflowStats>>[\"data\"];\n  const [statistics, setStatistics] = useState<Statistics>();\n\n  const { loading, cancel } = useRequest(\n    () => {\n      return getWorkflowStats();\n    },\n    {\n      pollingInterval: 3000,\n      pollingWhenHidden: false,\n      throttleWait: 1000,\n      throttleLeading: true,\n      onSuccess: (res) => {\n        setStatistics(res.data);\n      },\n      onError: () => {\n        if (!statistics) {\n          cancel();\n        }\n      },\n    }\n  );\n\n  return (\n    <div className={className} style={style}>\n      <div className=\"flex w-full flex-wrap items-stretch justify-center gap-4 sm:flex-nowrap\">\n        <Card className=\"w-full sm:flex-1 md:w-1/3\" loading={loading && !statistics}>\n          <Statistic title={t(\"settings.diagnostics.workflow_dispatcher.statistics.concurrency\")} value={statistics?.concurrency ?? \"-\"} />\n        </Card>\n\n        <Tooltip\n          mouseEnterDelay={1}\n          placement=\"topLeft\"\n          title={\n            statistics?.pendingRunIds?.length\n              ? statistics?.pendingRunIds?.map((id) => (\n                  <div key={id}>\n                    <Tag>#{id}</Tag>\n                  </div>\n                ))\n              : null\n          }\n        >\n          <Card className=\"w-full sm:flex-1 md:w-1/3\" loading={loading && !statistics}>\n            <Statistic title={t(\"settings.diagnostics.workflow_dispatcher.statistics.pending\")} value={statistics?.pendingRunIds?.length ?? \"-\"} />\n          </Card>\n        </Tooltip>\n\n        <Tooltip\n          mouseEnterDelay={1}\n          placement=\"topLeft\"\n          title={\n            statistics?.processingRunIds?.length\n              ? statistics?.processingRunIds?.map((id) => (\n                  <div key={id}>\n                    <Tag>#{id}</Tag>\n                  </div>\n                ))\n              : null\n          }\n        >\n          <Card className=\"w-full sm:flex-1 md:w-1/3\" loading={loading && !statistics}>\n            <Statistic title={t(\"settings.diagnostics.workflow_dispatcher.statistics.processing\")} value={statistics?.processingRunIds?.length ?? \"-\"} />\n          </Card>\n        </Tooltip>\n      </div>\n    </div>\n  );\n};\n\nexport default SettingsDiagnostics;\n"
  },
  {
    "path": "ui/src/pages/settings/SettingsPersistence.tsx",
    "content": "import { createContext, useContext, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useMount } from \"ahooks\";\nimport { App, Button, Divider, Form, InputNumber, Skeleton } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { produce } from \"immer\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport { type PersistenceSettingsContent } from \"@/domain/settings\";\nimport { useAntdForm, useZustandShallowSelector } from \"@/hooks\";\nimport { usePersistenceSettingsStore } from \"@/stores/settings\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nconst SettingsPersistence = () => {\n  const { t } = useTranslation();\n\n  const { message, notification } = App.useApp();\n\n  const { settings, loading, loadSettings, saveSettings } = usePersistenceSettingsStore(\n    useZustandShallowSelector([\"settings\", \"loading\", \"loadSettings\", \"saveSettings\"])\n  );\n  useMount(() => loadSettings());\n\n  const updateContextSettings = async (settings: PersistenceSettingsContent) => {\n    try {\n      await saveSettings(settings);\n\n      message.success(t(\"common.text.operation_succeeded\"));\n    } catch (err) {\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n    }\n  };\n\n  return (\n    <InternalSettingsContext.Provider\n      value={{\n        loading: loading,\n        settings: settings!,\n        updateSettings: updateContextSettings,\n      }}\n    >\n      <h2>{t(\"settings.persistence.alerting.title\")}</h2>\n      <SettingsPersistenceAlerting className=\"md:max-w-160\" />\n\n      <Divider />\n\n      <h2>{t(\"settings.persistence.data_retention.title\")}</h2>\n      <SettingsPersistenceDataRetention className=\"md:max-w-160\" />\n    </InternalSettingsContext.Provider>\n  );\n};\n\nconst SettingsPersistenceAlerting = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {\n  const { t } = useTranslation();\n\n  const { loading, settings, updateSettings } = useContext(InternalSettingsContext);\n\n  const formSchema = z.object({\n    certificatesWarningDaysBeforeExpire: z.number().int().positive(),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const {\n    form: formInst,\n    formPending,\n    formProps,\n  } = useAntdForm<z.infer<typeof formSchema>>({\n    initialValues: {\n      certificatesWarningDaysBeforeExpire: settings?.certificatesWarningDaysBeforeExpire,\n    },\n    onSubmit: async (values) => {\n      updateSettings(\n        produce(settings!, (draft) => {\n          draft.certificatesWarningDaysBeforeExpire = values.certificatesWarningDaysBeforeExpire;\n        })\n      );\n    },\n  });\n  const [formChanged, setFormChanged] = useState(false);\n\n  const handleInputChange = () => {\n    const changed = formInst.getFieldValue(\"certificatesWarningDaysBeforeExpire\") !== formProps.initialValues?.certificatesWarningDaysBeforeExpire;\n    setFormChanged(changed);\n  };\n\n  return (\n    <>\n      <div className={className} style={style}>\n        <Show when={!loading} fallback={<Skeleton active />}>\n          <Form {...formProps} form={formInst} disabled={formPending} layout=\"vertical\">\n            <Form.Item\n              name=\"certificatesWarningDaysBeforeExpire\"\n              label={t(\"settings.persistence.alerting.form.certificates_warning_days_before_expire.label\")}\n              extra={<span dangerouslySetInnerHTML={{ __html: t(\"settings.persistence.alerting.form.certificates_warning_days_before_expire.help\") }}></span>}\n              rules={[formRule]}\n            >\n              <InputNumber\n                style={{ width: \"100%\" }}\n                min={1}\n                max={365}\n                placeholder={t(\"settings.persistence.alerting.form.certificates_warning_days_before_expire.placeholder\")}\n                suffix={t(\"settings.persistence.alerting.form.certificates_warning_days_before_expire.unit\")}\n                onChange={handleInputChange}\n              />\n            </Form.Item>\n\n            <Form.Item>\n              <Button type=\"primary\" htmlType=\"submit\" disabled={!formChanged} loading={formPending}>\n                {t(\"common.button.save\")}\n              </Button>\n            </Form.Item>\n          </Form>\n        </Show>\n      </div>\n    </>\n  );\n};\n\nconst SettingsPersistenceDataRetention = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {\n  const { t } = useTranslation();\n\n  const { loading, settings, updateSettings } = useContext(InternalSettingsContext);\n\n  const formSchema = z.object({\n    certificatesRetentionMaxDays: z.number().int().nonnegative(),\n    workflowRunsRetentionMaxDays: z.number().int().nonnegative(),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const {\n    form: formInst,\n    formPending,\n    formProps,\n  } = useAntdForm<z.infer<typeof formSchema>>({\n    initialValues: {\n      certificatesRetentionMaxDays: settings?.certificatesRetentionMaxDays,\n      workflowRunsRetentionMaxDays: settings?.workflowRunsRetentionMaxDays,\n    },\n    onSubmit: async (values) => {\n      updateSettings(\n        produce(settings!, (draft) => {\n          draft.certificatesRetentionMaxDays = values.certificatesRetentionMaxDays;\n          draft.workflowRunsRetentionMaxDays = values.workflowRunsRetentionMaxDays;\n        })\n      );\n    },\n  });\n  const [formChanged, setFormChanged] = useState(false);\n\n  const handleInputChange = () => {\n    const changed =\n      formInst.getFieldValue(\"certificatesRetentionMaxDays\") !== formProps.initialValues?.certificatesRetentionMaxDays ||\n      formInst.getFieldValue(\"workflowRunsRetentionMaxDays\") !== formProps.initialValues?.workflowRunsRetentionMaxDays;\n    setFormChanged(changed);\n  };\n\n  return (\n    <>\n      <div className={className} style={style}>\n        <Show when={!loading} fallback={<Skeleton active />}>\n          <Form {...formProps} form={formInst} disabled={formPending} layout=\"vertical\">\n            <Form.Item\n              name=\"certificatesRetentionMaxDays\"\n              label={t(\"settings.persistence.data_retention.form.certificates_retention_max_days.label\")}\n              extra={<span dangerouslySetInnerHTML={{ __html: t(\"settings.persistence.data_retention.form.certificates_retention_max_days.help\") }}></span>}\n              rules={[formRule]}\n            >\n              <InputNumber\n                style={{ width: \"100%\" }}\n                min={0}\n                max={36500}\n                placeholder={t(\"settings.persistence.data_retention.form.certificates_retention_max_days.placeholder\")}\n                suffix={t(\"settings.persistence.data_retention.form.certificates_retention_max_days.unit\")}\n                onChange={handleInputChange}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"workflowRunsRetentionMaxDays\"\n              label={t(\"settings.persistence.data_retention.form.workflow_runs_retention_max_days.label\")}\n              extra={<span dangerouslySetInnerHTML={{ __html: t(\"settings.persistence.data_retention.form.workflow_runs_retention_max_days.help\") }}></span>}\n              rules={[formRule]}\n            >\n              <InputNumber\n                style={{ width: \"100%\" }}\n                min={0}\n                max={36500}\n                placeholder={t(\"settings.persistence.data_retention.form.workflow_runs_retention_max_days.placeholder\")}\n                suffix={t(\"settings.persistence.data_retention.form.workflow_runs_retention_max_days.unit\")}\n                onChange={handleInputChange}\n              />\n            </Form.Item>\n\n            <Form.Item>\n              <Button type=\"primary\" htmlType=\"submit\" disabled={!formChanged} loading={formPending}>\n                {t(\"common.button.save\")}\n              </Button>\n            </Form.Item>\n          </Form>\n        </Show>\n      </div>\n    </>\n  );\n};\n\nconst InternalSettingsContext = createContext(\n  {} as {\n    loading: boolean;\n    settings: PersistenceSettingsContent;\n    updateSettings: (settings: PersistenceSettingsContent) => Promise<void>;\n  }\n);\n\nexport default SettingsPersistence;\n"
  },
  {
    "path": "ui/src/pages/settings/SettingsSSLProvider.tsx",
    "content": "import { createContext, useContext, useEffect, useMemo, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useMount } from \"ahooks\";\nimport { App, Button, Card, Divider, Form, Input, Select, Skeleton } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport { produce } from \"immer\";\nimport { z } from \"zod\";\n\nimport Show from \"@/components/Show\";\nimport Tips from \"@/components/Tips\";\nimport { type CAProviderType, CA_PROVIDERS } from \"@/domain/provider\";\nimport { type SSLProviderSettingsContent } from \"@/domain/settings\";\nimport { useAntdForm, useZustandShallowSelector } from \"@/hooks\";\nimport { useSSLProviderSettingsStore } from \"@/stores/settings\";\nimport { mergeCls } from \"@/utils/css\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nconst SettingsSSLProvider = () => {\n  const { t } = useTranslation();\n\n  const { message, notification } = App.useApp();\n\n  const { settings, loading, loadSettings, saveSettings } = useSSLProviderSettingsStore(\n    useZustandShallowSelector([\"settings\", \"loading\", \"loadSettings\", \"saveSettings\"])\n  );\n  useMount(() => loadSettings());\n\n  const updateContextSettings = async (settings: SSLProviderSettingsContent) => {\n    try {\n      await saveSettings(settings);\n\n      message.success(t(\"common.text.operation_succeeded\"));\n    } catch (err) {\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n    }\n  };\n\n  return (\n    <InternalSettingsContext.Provider\n      value={{\n        loading: loading,\n        settings: settings!,\n        updateSettings: updateContextSettings,\n      }}\n    >\n      <h2>{t(\"settings.sslprovider.ca.title\")}</h2>\n      <SettingsSSLProviderCA />\n\n      <Divider />\n\n      <h2>{t(\"settings.sslprovider.others.title\")}</h2>\n      <SettingsSSLProviderOthers className=\"md:max-w-160\" />\n    </InternalSettingsContext.Provider>\n  );\n};\n\nconst SettingsSSLProviderCA = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {\n  const { t } = useTranslation();\n\n  const { loading, settings } = useContext(InternalSettingsContext);\n\n  const formSchema = z.object({\n    provider: z.string().nonempty(),\n    configs: z.object().nullish(),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const [formInst] = Form.useForm<z.infer<typeof formSchema>>();\n\n  const providers = [\n    [CA_PROVIDERS.LETSENCRYPT, \"provider.letsencrypt\", \"letsencrypt.org\", \"/imgs/providers/letsencrypt.svg\"],\n    [CA_PROVIDERS.LETSENCRYPTSTAGING, \"provider.letsencryptstaging\", \"letsencrypt.org\", \"/imgs/providers/letsencrypt.svg\"],\n    [CA_PROVIDERS.ACTALISSSL, \"provider.actalisssl\", \"actalis.com\", \"/imgs/providers/actalisssl.png\"],\n    [CA_PROVIDERS.GLOBALSIGNATLAS, \"provider.globalsignatlas\", \"atlas.globalsign.com\", \"/imgs/providers/globalsignatlas.png\"],\n    [CA_PROVIDERS.GOOGLETRUSTSERVICES, \"provider.googletrustservices\", \"pki.goog\", \"/imgs/providers/google.svg\"],\n    [CA_PROVIDERS.SECTIGO, \"provider.sectigo\", \"sectigo.com\", \"/imgs/providers/sectigo.svg\"],\n    [CA_PROVIDERS.SSLCOM, \"provider.sslcom\", \"ssl.com\", \"/imgs/providers/sslcom.svg\"],\n    [CA_PROVIDERS.ZEROSSL, \"provider.zerossl\", \"zerossl.com\", \"/imgs/providers/zerossl.svg\"],\n    [CA_PROVIDERS.LITESSL, \"provider.litessl\", \"litessl.cn (freessl.cn)\", \"/imgs/providers/litessl.svg\"],\n    [CA_PROVIDERS.ACMECA, \"provider.acmeca\", \"ACME v2 (RFC 8555)\", \"/imgs/providers/acmeca.svg\"],\n  ].map(([value, name, description, icon]) => {\n    return {\n      value: value as CAProviderType,\n      name: t(name),\n      description,\n      icon,\n    };\n  });\n  const [providerValue, setProviderValue] = useState(settings.provider);\n\n  const renderSiblingFieldProviderComponent = useMemo(() => {\n    switch (providerValue) {\n      case CA_PROVIDERS.LETSENCRYPT:\n        return <InternalCASettingsFormProviderLetsEncrypt />;\n      case CA_PROVIDERS.LETSENCRYPTSTAGING:\n        return <InternalCASettingsFormProviderLetsEncryptStaging />;\n      case CA_PROVIDERS.ACTALISSSL:\n        return <InternalCASettingsFormProviderActalisSSL />;\n      case CA_PROVIDERS.GLOBALSIGNATLAS:\n        return <InternalCASettingsFormProviderGlobalSignAtlas />;\n      case CA_PROVIDERS.GOOGLETRUSTSERVICES:\n        return <InternalCASettingsFormProviderGoogleTrustServices />;\n      case CA_PROVIDERS.LITESSL:\n        return <InternalCASettingsFormProviderLiteSSL />;\n      case CA_PROVIDERS.SECTIGO:\n        return <InternalCASettingsFormProviderSectigo />;\n      case CA_PROVIDERS.SSLCOM:\n        return <InternalCASettingsFormProviderSSLCom />;\n      case CA_PROVIDERS.ZEROSSL:\n        return <InternalCASettingsFormProviderZeroSSL />;\n      case CA_PROVIDERS.ACMECA:\n        return <InternalCASettingsFormProviderACMECA />;\n    }\n  }, [providerValue]);\n\n  useEffect(() => {\n    setProviderValue(settings.provider);\n  }, [settings.provider]);\n\n  return (\n    <div className={className} style={style}>\n      <Show when={!loading} fallback={<Skeleton active />}>\n        <Form form={formInst} layout=\"vertical\" initialValues={{ provider: providerValue }}>\n          <Form.Item>\n            <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"settings.sslprovider.ca.tips\") }}></span>} />\n          </Form.Item>\n\n          <Form.Item\n            name=\"provider\"\n            label={t(\"settings.sslprovider.ca.form.provider.label\")}\n            extra={t(\"settings.sslprovider.ca.form.provider.help\")}\n            rules={[formRule]}\n          >\n            <div className=\"flex w-full flex-wrap items-center gap-4\">\n              {providers.map((provider) => (\n                <Card\n                  key={provider.value}\n                  className={mergeCls(\"relative overflow-hidden\", { [\"border-primary\"]: providerValue === provider.value })}\n                  style={{ width: 280 }}\n                  styles={{\n                    body: { padding: 0 },\n                  }}\n                  hoverable\n                  onClick={() => setProviderValue(provider.value)}\n                >\n                  <div className=\"relative z-1 px-3 py-4\">\n                    <div className=\"flex items-center justify-between gap-3\">\n                      <div>\n                        <img src={provider.icon} className=\"size-8\" />\n                      </div>\n                      <div className=\"flex-1 overflow-hidden\">\n                        <div className=\"truncate\">{provider.name}</div>\n                        <div className=\"mt-1 truncate text-xs\">{provider.description}</div>\n                      </div>\n                    </div>\n                  </div>\n                  {providerValue === provider.value && <div className=\"absolute top-0 left-0 size-full bg-primary opacity-20\"></div>}\n                </Card>\n              ))}\n            </div>\n          </Form.Item>\n        </Form>\n\n        <div className=\"md:max-w-160\">{renderSiblingFieldProviderComponent}</div>\n      </Show>\n    </div>\n  );\n};\n\nconst SettingsSSLProviderOthers = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {\n  const { t } = useTranslation();\n\n  const { loading, settings, updateSettings } = useContext(InternalSettingsContext);\n\n  const formSchema = z.object({\n    timeout: z.union([z.string(), z.number().int().positive()]).nullish(),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const { form: formInst, formProps } = useAntdForm<z.infer<typeof formSchema>>({\n    initialValues: { timeout: settings.timeout },\n    onSubmit: async (values) => {\n      setFormPending(true);\n\n      try {\n        const newSettings = produce(settings, (draft) => {\n          draft.timeout = +values.timeout!;\n        });\n        await updateSettings(newSettings);\n      } finally {\n        setFormPending(false);\n      }\n\n      setFormChanged(false);\n    },\n  });\n  const [formPending, setFormPending] = useState(false);\n  const [formChanged, setFormChanged] = useState(false);\n\n  const handleFormChange = () => {\n    setFormChanged(true);\n  };\n\n  return (\n    <div className={className} style={style}>\n      <Show when={!loading} fallback={<Skeleton active />}>\n        <Form {...formProps} form={formInst} disabled={formPending} layout=\"vertical\" onValuesChange={handleFormChange}>\n          <Form.Item\n            name=\"timeout\"\n            label={t(\"settings.sslprovider.others.form.timeout.label\")}\n            rules={[formRule]}\n            tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"settings.sslprovider.others.form.timeout.tooltip\") }}></span>}\n          >\n            <Input\n              type=\"number\"\n              allowClear\n              min={0}\n              max={3600}\n              placeholder={t(\"settings.sslprovider.others.form.timeout.placeholder\")}\n              suffix={t(\"settings.sslprovider.others.form.timeout.unit\")}\n            />\n          </Form.Item>\n\n          <Form.Item>\n            <Button type=\"primary\" htmlType=\"submit\" disabled={!formChanged} loading={formPending}>\n              {t(\"common.button.save\")}\n            </Button>\n          </Form.Item>\n        </Form>\n      </Show>\n    </div>\n  );\n};\n\nconst InternalSettingsContext = createContext(\n  {} as {\n    loading: boolean;\n    settings: SSLProviderSettingsContent;\n    updateSettings: (settings: SSLProviderSettingsContent) => Promise<void>;\n  }\n);\n\nconst InternalCASharedForm = ({ children, provider }: { children?: React.ReactNode; provider: CAProviderType }) => {\n  const { t } = useTranslation();\n\n  const { settings, updateSettings } = useContext(InternalSettingsContext);\n\n  const { form: formInst, formProps } = useAntdForm<NonNullable<unknown>>({\n    initialValues: settings?.configs?.[provider],\n    onSubmit: async (values) => {\n      setFormPending(true);\n\n      try {\n        const newSettings = produce(settings, (draft) => {\n          draft.provider = provider;\n          draft.configs ??= {} as SSLProviderSettingsContent[\"configs\"];\n          draft.configs[provider] = values;\n        });\n        await updateSettings(newSettings);\n      } finally {\n        setFormPending(false);\n      }\n\n      setFormChanged(false);\n    },\n  });\n  const [formPending, setFormPending] = useState(false);\n  const [formChanged, setFormChanged] = useState(false);\n\n  useEffect(() => {\n    setFormChanged(provider !== settings?.provider);\n  }, [provider, settings?.provider]);\n\n  const handleFormChange = () => {\n    setFormChanged(true);\n  };\n\n  return (\n    <Form {...formProps} form={formInst} disabled={formPending} layout=\"vertical\" onValuesChange={handleFormChange}>\n      {children}\n\n      <Form.Item>\n        <Button type=\"primary\" htmlType=\"submit\" disabled={!formChanged} loading={formPending}>\n          {t(\"common.button.save\")}\n        </Button>\n      </Form.Item>\n    </Form>\n  );\n};\n\nconst InternalCASharedFormEabFields = ({ i18nKey }: { i18nKey: string }) => {\n  const { t, i18n } = useTranslation();\n\n  const hasGuide = i18n.exists(`access.form.${i18nKey}_eab.guide`);\n\n  const formSchema = z.object({\n    endpoint: z.url(t(\"common.errmsg.url_invalid\")),\n    eabKid: z.string(t(\"access.form.shared_acme_eab_kid.label\")).nonempty(t(\"access.form.shared_acme_eab_kid.placeholder\")),\n    eabHmacKey: z.string(t(\"access.form.shared_acme_eab_hmac_key.label\")).nonempty(t(\"access.form.shared_acme_eab_hmac_key.placeholder\")),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n\n  return (\n    <>\n      <Form.Item name=\"eabKid\" label={t(\"access.form.shared_acme_eab_kid.label\")} rules={[formRule]}>\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_kid.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name=\"eabHmacKey\" label={t(\"access.form.shared_acme_eab_hmac_key.label\")} rules={[formRule]}>\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.shared_acme_eab_hmac_key.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item hidden={!hasGuide}>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(`access.form.${i18nKey}_eab.guide`) }}></span>} />\n      </Form.Item>\n    </>\n  );\n};\n\nconst InternalCASettingsFormProviderLetsEncrypt = () => {\n  return <InternalCASharedForm provider={CA_PROVIDERS.LETSENCRYPT} />;\n};\n\nconst InternalCASettingsFormProviderLetsEncryptStaging = () => {\n  const { t } = useTranslation();\n\n  return (\n    <InternalCASharedForm provider={CA_PROVIDERS.LETSENCRYPTSTAGING}>\n      <Form.Item>\n        <Tips message={<span dangerouslySetInnerHTML={{ __html: t(\"settings.sslprovider.ca.form.letsencryptstaging_alert\") }}></span>} />\n      </Form.Item>\n    </InternalCASharedForm>\n  );\n};\n\nconst InternalCASettingsFormProviderActalisSSL = () => {\n  return (\n    <InternalCASharedForm provider={CA_PROVIDERS.ACTALISSSL}>\n      <InternalCASharedFormEabFields i18nKey=\"actalisssl\" />\n    </InternalCASharedForm>\n  );\n};\n\nconst InternalCASettingsFormProviderGlobalSignAtlas = () => {\n  return (\n    <InternalCASharedForm provider={CA_PROVIDERS.GLOBALSIGNATLAS}>\n      <InternalCASharedFormEabFields i18nKey=\"globalsignatlas\" />\n    </InternalCASharedForm>\n  );\n};\n\nconst InternalCASettingsFormProviderGoogleTrustServices = () => {\n  return (\n    <InternalCASharedForm provider={CA_PROVIDERS.GOOGLETRUSTSERVICES}>\n      <InternalCASharedFormEabFields i18nKey=\"googletrustservices\" />\n    </InternalCASharedForm>\n  );\n};\n\nconst InternalCASettingsFormProviderLiteSSL = () => {\n  return (\n    <InternalCASharedForm provider={CA_PROVIDERS.LITESSL}>\n      <InternalCASharedFormEabFields i18nKey=\"litessl\" />\n    </InternalCASharedForm>\n  );\n};\n\nconst InternalCASettingsFormProviderSectigo = () => {\n  const { t } = useTranslation();\n\n  const formSchema = z.object({\n    validationType: z.string().nonempty(t(\"access.form.sectigo_validation_type.placeholder\")),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n\n  return (\n    <InternalCASharedForm provider={CA_PROVIDERS.SECTIGO}>\n      <Form.Item name=\"validationType\" initialValue=\"dv\" label={t(\"access.form.sectigo_validation_type.label\")} rules={[formRule]}>\n        <Select\n          options={[\"dv\", \"ov\", \"ev\"].map((s) => ({\n            key: s,\n            label: t(`access.form.sectigo_validation_type.option.${s}.label`),\n            value: s,\n          }))}\n          placeholder={t(\"access.form.sectigo_validation_type.placeholder\")}\n        />\n      </Form.Item>\n\n      <InternalCASharedFormEabFields i18nKey=\"sectigo\" />\n    </InternalCASharedForm>\n  );\n};\n\nconst InternalCASettingsFormProviderSSLCom = () => {\n  return (\n    <InternalCASharedForm provider={CA_PROVIDERS.SSLCOM}>\n      <InternalCASharedFormEabFields i18nKey=\"sslcom\" />\n    </InternalCASharedForm>\n  );\n};\n\nconst InternalCASettingsFormProviderZeroSSL = () => {\n  return (\n    <InternalCASharedForm provider={CA_PROVIDERS.ZEROSSL}>\n      <InternalCASharedFormEabFields i18nKey=\"zerossl\" />\n    </InternalCASharedForm>\n  );\n};\n\nconst InternalCASettingsFormProviderACMECA = () => {\n  const { t } = useTranslation();\n\n  const formSchema = z.object({\n    endpoint: z.url(t(\"common.errmsg.url_invalid\")),\n    eabKid: z.string(t(\"access.form.acmeca_eab_kid.placeholder\")).nullish(),\n    eabHmacKey: z.string(t(\"access.form.acmeca_eab_hmac_key.placeholder\")).nullish(),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n\n  return (\n    <InternalCASharedForm provider={CA_PROVIDERS.ACMECA}>\n      <Form.Item\n        name=\"endpoint\"\n        label={t(\"access.form.acmeca_endpoint.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.acmeca_endpoint.tooltip\") }}></span>}\n      >\n        <Input placeholder={t(\"access.form.acmeca_endpoint.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name=\"eabKid\"\n        label={t(\"access.form.acmeca_eab_kid.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.acmeca_eab_kid.tooltip\") }}></span>}\n      >\n        <Input autoComplete=\"new-password\" placeholder={t(\"access.form.acmeca_eab_kid.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item\n        name=\"eabHmacKey\"\n        label={t(\"access.form.acmeca_eab_hmac_key.label\")}\n        rules={[formRule]}\n        tooltip={<span dangerouslySetInnerHTML={{ __html: t(\"access.form.acmeca_eab_hmac_key.tooltip\") }}></span>}\n      >\n        <Input.Password autoComplete=\"new-password\" placeholder={t(\"access.form.acmeca_eab_hmac_key.placeholder\")} />\n      </Form.Item>\n    </InternalCASharedForm>\n  );\n};\n\nexport default SettingsSSLProvider;\n"
  },
  {
    "path": "ui/src/pages/workflows/WorkflowDetail.tsx",
    "content": "import { useEffect, useMemo, useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Outlet, useLocation, useNavigate, useParams } from \"react-router-dom\";\nimport { IconEdit, IconHistory, IconPlayerPlay, IconRobot } from \"@tabler/icons-react\";\nimport { useSize } from \"ahooks\";\nimport { App, Button, Input, type InputRef, Segmented, Skeleton, Spin } from \"antd\";\n\nimport { startRun as startWorkflowRun } from \"@/api/workflows\";\nimport Show from \"@/components/Show\";\nimport { WORKFLOW_RUN_STATUSES } from \"@/domain/workflowRun\";\nimport { useZustandShallowSelector } from \"@/hooks\";\nimport { useWorkflowStore } from \"@/stores/workflow\";\nimport { mergeCls } from \"@/utils/css\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nconst WorkflowDetail = () => {\n  const location = useLocation();\n  const navigate = useNavigate();\n\n  const { t } = useTranslation();\n\n  const { message, modal, notification } = App.useApp();\n\n  const { id: workflowId } = useParams();\n  const { workflow, initialized, ...workflowState } = useWorkflowStore(useZustandShallowSelector([\"workflow\", \"initialized\", \"init\", \"destroy\", \"setEnabled\"]));\n  useEffect(() => {\n    Promise.try(() => workflowState.init(workflowId!)).catch((err) => {\n      console.error(err);\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n    });\n\n    return () => {\n      workflowState.destroy();\n    };\n  }, [workflowId]);\n\n  const divHeaderRef = useRef<HTMLDivElement>(null);\n  const divHeaderSize = useSize(divHeaderRef);\n\n  const tabs = [\n    [\"design\", \"workflow.detail.design.tab\", <IconRobot size=\"1em\" />],\n    [\"runs\", \"workflow.detail.runs.tab\", <IconHistory size=\"1em\" />],\n  ] satisfies [string, string, React.ReactElement][];\n  const [tabValue, setTabValue] = useState<string>(() => location.pathname.split(\"/\")[3]);\n  useEffect(() => {\n    const subpath = location.pathname.split(\"/\")[3];\n    if (!subpath) {\n      navigate(`/workflows/${workflowId}/${tabs[0][0]}`, { replace: true });\n      return;\n    }\n\n    setTabValue(subpath);\n  }, [location.pathname, workflowId]);\n\n  const handleTabChange = (value: string) => {\n    setTabValue(value);\n    navigate(`/workflows/${workflowId}/${value}`);\n  };\n\n  const runButtonDisabled = useMemo(() => !workflow.hasContent, [workflow]);\n  const [runButtonLoading, setRunButtonLoading] = useState(false);\n  useEffect(() => {\n    const running = workflow.lastRunStatus === WORKFLOW_RUN_STATUSES.PENDING || workflow.lastRunStatus === WORKFLOW_RUN_STATUSES.PROCESSING;\n    setRunButtonLoading(running);\n  }, [workflow.lastRunStatus]);\n\n  const handleRunClick = () => {\n    const { promise, resolve } = Promise.withResolvers();\n    if (workflow.hasDraft) {\n      modal.confirm({\n        title: t(\"workflow.action.execute.modal.title\"),\n        content: t(\"workflow.action.execute.modal.content\"),\n        onOk: () => resolve(void 0),\n      });\n    } else {\n      resolve(void 0);\n    }\n\n    promise.then(async () => {\n      try {\n        setRunButtonLoading(true);\n\n        await startWorkflowRun(workflow.id);\n\n        message.info(t(\"workflow.action.execute.prompt\"));\n      } catch (err) {\n        setRunButtonLoading(false);\n\n        console.error(err);\n        message.warning(t(\"common.text.operation_failed\"));\n      }\n    });\n  };\n\n  const handleActiveClick = async () => {\n    try {\n      if (!workflow.enabled && !workflow.graphContent) {\n        message.warning(t(\"workflow.action.enable.errmsg.unpublished\"));\n        return;\n      }\n\n      await workflowState.setEnabled(!workflow.enabled);\n    } catch (err) {\n      console.error(err);\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n    }\n  };\n\n  return (\n    <div className=\"flex size-full flex-col\">\n      <div className=\"px-6 py-4\" ref={divHeaderRef}>\n        <div className=\"container relative z-11 flex justify-between gap-4 not-md:flex-wrap\">\n          <div className=\"flex-1 not-md:w-full not-md:flex-none\">\n            <WorkflowDetailBaseName />\n            <WorkflowDetailBaseDescription />\n\n            <div className=\"absolute -bottom-12 left-1/2 z-1 -translate-x-1/2\">\n              <Segmented\n                className=\"shadow-sm\"\n                options={tabs.map(([key, label, icon]) => ({\n                  value: key,\n                  label: <span className=\"px-2 text-sm\">{t(label)}</span>,\n                  icon: (\n                    <span className=\"anticon scale-125\" role=\"img\">\n                      {icon}\n                    </span>\n                  ),\n                }))}\n                size=\"large\"\n                value={tabValue}\n                onChange={handleTabChange}\n              />\n            </div>\n          </div>\n          <div className=\"not-md:mb-2 not-md:w-full\">\n            <Show when={initialized}>\n              <div className=\"flex items-center gap-2 not-md:justify-end\">\n                <Button onClick={handleActiveClick}>{workflow.enabled ? t(\"workflow.action.disable.button\") : t(\"workflow.action.enable.button\")}</Button>\n                <Button disabled={runButtonDisabled} icon={<IconPlayerPlay size=\"1.25em\" />} loading={runButtonLoading} type=\"primary\" onClick={handleRunClick}>\n                  {t(\"workflow.action.execute.button\")}\n                </Button>\n              </div>\n            </Show>\n          </div>\n        </div>\n      </div>\n\n      <div\n        className=\"flex-1 p-4\"\n        style={{\n          minHeight: `calc(max(360px, 100% - ${divHeaderSize?.height ?? 0}px))`,\n        }}\n      >\n        <Show\n          when={initialized}\n          fallback={\n            <div className=\"container pt-12\">\n              <Skeleton active />\n            </div>\n          }\n        >\n          <Outlet />\n        </Show>\n      </div>\n    </div>\n  );\n};\n\nconst WorkflowDetailBaseName = () => {\n  const { t } = useTranslation();\n\n  const { notification } = App.useApp();\n\n  const { workflow, initialized, ...workflowStore } = useWorkflowStore(useZustandShallowSelector([\"workflow\", \"initialized\", \"setName\"]));\n\n  const inputRef = useRef<InputRef>(null);\n  const [editing, setEditing] = useState(false);\n  const [value, setValue] = useState(\"\");\n\n  useEffect(() => {\n    setEditing(false);\n  }, [workflow.id]);\n\n  const handleEditClick = () => {\n    if (!initialized) return;\n\n    setEditing(true);\n    setValue(workflow.name);\n    setTimeout(() => {\n      inputRef.current?.focus({ cursor: \"all\" });\n    }, 0);\n  };\n\n  const handleValueChange = (value: string) => {\n    setValue(value);\n  };\n\n  const handleValueConfirm = async (value: string) => {\n    value = value.trim();\n    if (!value || value === (workflow.name || \"\")) {\n      setEditing(false);\n      return;\n    }\n\n    setEditing(false);\n\n    try {\n      await workflowStore.setName(value);\n    } catch (err) {\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n\n      throw err;\n    }\n  };\n\n  return (\n    <div className=\"group/input relative flex items-center gap-1\">\n      <h1 className={mergeCls(\"break-all\", { invisible: editing })} onDoubleClick={handleEditClick}>\n        <Show when={initialized} fallback={<Spin />}>\n          {workflow.name || t(\"workflow.detail.baseinfo.name.placeholder\")}\n        </Show>\n      </h1>\n      <Show when={initialized}>\n        <Button\n          className={mergeCls(\"mb-2 opacity-0 transition-opacity group-hover/input:opacity-100\", {\n            invisible: editing,\n          })}\n          icon={<IconEdit size=\"1.25em\" stroke=\"1.25\" />}\n          type=\"text\"\n          onClick={handleEditClick}\n        />\n      </Show>\n      <Input\n        className={mergeCls(\"absolute top-0 left-0\", editing ? \"block\" : \"hidden\")}\n        ref={inputRef}\n        maxLength={100}\n        placeholder={t(\"workflow.detail.baseinfo.name.placeholder\")}\n        size=\"large\"\n        value={value}\n        variant=\"filled\"\n        onBlur={(e) => handleValueConfirm(e.target.value)}\n        onChange={(e) => handleValueChange(e.target.value)}\n        onPressEnter={(e) => e.currentTarget.blur()}\n      />\n    </div>\n  );\n};\n\nconst WorkflowDetailBaseDescription = () => {\n  const { t } = useTranslation();\n\n  const { notification } = App.useApp();\n\n  const { workflow, initialized, ...workflowStore } = useWorkflowStore(useZustandShallowSelector([\"workflow\", \"initialized\", \"setDescription\"]));\n\n  const inputRef = useRef<InputRef>(null);\n  const [editing, setEditing] = useState(false);\n  const [value, setValue] = useState(\"\");\n\n  useEffect(() => {\n    setEditing(false);\n  }, [workflow.id]);\n\n  const handleEditClick = () => {\n    if (!initialized) return;\n\n    setEditing(true);\n    setValue(workflow.description || \"\");\n    setTimeout(() => {\n      inputRef.current?.focus({ cursor: \"all\" });\n    }, 0);\n  };\n\n  const handleValueChange = (value: string) => {\n    setValue(value);\n  };\n\n  const handleValueConfirm = async (value: string) => {\n    value = value.trim();\n    if (!value || value === (workflow.description || \"\")) {\n      setEditing(false);\n      return;\n    }\n\n    setEditing(false);\n\n    try {\n      await workflowStore.setDescription(value);\n    } catch (err) {\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n\n      throw err;\n    }\n  };\n\n  return (\n    <div className=\"group/input relative flex items-center gap-1\">\n      <p className={mergeCls(\"text-base text-gray-500\", { invisible: editing })} onDoubleClick={handleEditClick}>\n        <Show when={initialized} fallback={\"\\u00A0\"}>\n          {workflow.description || t(\"workflow.detail.baseinfo.description.placeholder\")}\n        </Show>\n      </p>\n      <Show when={initialized}>\n        <Button\n          className={mergeCls(\"mb-4 opacity-0 transition-opacity group-hover/input:opacity-100\", {\n            invisible: editing,\n          })}\n          icon={<IconEdit size=\"1.25em\" stroke=\"1.25\" />}\n          type=\"text\"\n          onClick={handleEditClick}\n        />\n      </Show>\n      <Input\n        className={mergeCls(\"absolute top-0 left-0\", editing ? \"block\" : \"hidden\")}\n        ref={inputRef}\n        maxLength={100}\n        placeholder={t(\"workflow.detail.baseinfo.description.placeholder\")}\n        value={value}\n        variant=\"filled\"\n        onBlur={(e) => handleValueConfirm(e.target.value)}\n        onChange={(e) => handleValueChange(e.target.value)}\n        onPressEnter={(e) => e.currentTarget.blur()}\n      />\n    </div>\n  );\n};\n\nexport default WorkflowDetail;\n"
  },
  {
    "path": "ui/src/pages/workflows/WorkflowDetailDesign.tsx",
    "content": "import { useMemo, useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { FlowLayoutDefault } from \"@flowgram.ai/fixed-layout-editor\";\nimport { IconArrowBackUp, IconDots, IconTransferIn, IconTransferOut } from \"@tabler/icons-react\";\nimport { useDeepCompareEffect } from \"ahooks\";\nimport { Alert, App, Button, Card, Dropdown, Result, Space, theme } from \"antd\";\nimport { debounce } from \"radash\";\n\nimport Show from \"@/components/Show\";\nimport { WorkflowDesigner, type WorkflowDesignerInstance, WorkflowNodeDrawer, WorkflowToolbar } from \"@/components/workflow/designer\";\nimport WorkflowGraphExportModal from \"@/components/workflow/WorkflowGraphExportModal\";\nimport WorkflowGraphImportModal from \"@/components/workflow/WorkflowGraphImportModal\";\nimport { useAppSettings, useZustandShallowSelector } from \"@/hooks\";\nimport { useWorkflowStore } from \"@/stores/workflow\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nconst WorkflowDetailDesign = () => {\n  const { t } = useTranslation();\n\n  const { token: themeToken } = theme.useToken();\n  const { message, modal, notification } = App.useApp();\n\n  const { appSettings: globalAppSettings } = useAppSettings();\n\n  const { workflow, ...workflowStore } = useWorkflowStore(useZustandShallowSelector([\"workflow\", \"orchestrate\", \"publish\", \"rollback\"]));\n\n  const workflowRollbackDisabled = useMemo(() => !workflow.hasDraft || !workflow.hasContent, [workflow.hasDraft, workflow.hasContent]);\n  const workflowPublishDisabled = useMemo(() => !workflow.hasDraft, [workflow.hasDraft]);\n\n  const designerRef = useRef<WorkflowDesignerInstance>(null);\n  const designerPending = useRef(false); // 保存中时阻止刷新画布\n  const [designerError, setDesignerError] = useState<unknown>();\n  useDeepCompareEffect(() => {\n    if (designerRef.current == null || designerRef.current.document.disposed) return;\n    if (designerPending.current) return;\n\n    try {\n      const graph = workflow.graphDraft ?? { nodes: [] };\n      designerRef.current!.document.fromJSON(graph);\n      setDesignerError(void 0);\n    } catch (err) {\n      console.error(err);\n      setDesignerError(err);\n    }\n  }, [workflow.graphDraft]);\n\n  const { drawerProps: designerNodeDrawerProps, ...designerNodeDrawer } = WorkflowNodeDrawer.useDrawer();\n\n  const handleDesignerDocumentChange = debounce({ delay: 300 }, async () => {\n    if (designerRef.current == null || designerRef.current.document.disposed) return;\n\n    designerPending.current = true;\n    try {\n      const graph = designerRef.current!.document.toJSON();\n      await workflowStore.orchestrate(graph);\n    } catch (err) {\n      console.error(err);\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n    } finally {\n      designerPending.current = false;\n    }\n  });\n\n  const handleRollbackClick = () => {\n    modal.confirm({\n      title: t(\"workflow.detail.design.action.rollback.modal.title\"),\n      content: t(\"workflow.detail.design.action.rollback.modal.content\"),\n      onOk: async () => {\n        try {\n          await workflowStore.rollback();\n\n          message.success(t(\"common.text.operation_succeeded\"));\n        } catch (err) {\n          console.error(err);\n          notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n        }\n      },\n    });\n  };\n\n  const handlePublishClick = async () => {\n    if (!(await designerRef.current!.validateAllNodes())) {\n      message.warning(t(\"workflow.detail.design.uncompleted_design.alert\"));\n      return;\n    }\n\n    modal.confirm({\n      title: t(\"workflow.detail.design.action.publish.modal.title\"),\n      content: t(\"workflow.detail.design.action.publish.modal.content\"),\n      onOk: async () => {\n        try {\n          await workflowStore.publish();\n\n          message.success(t(\"common.text.operation_succeeded\"));\n        } catch (err) {\n          console.error(err);\n          notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n        }\n      },\n    });\n  };\n\n  const { modalProps: graphImportModalProps, ...graphImportModal } = WorkflowGraphImportModal.useModal();\n  const { modalProps: graphExportModalProps, ...graphExportModal } = WorkflowGraphExportModal.useModal();\n\n  const handleImportClick = async () => {\n    graphImportModal.open().then(async (graph) => {\n      const loadingKey = Math.random().toString(36).substring(0, 8);\n      message.loading({ key: loadingKey, content: t(\"common.text.saving\"), duration: 0 });\n\n      try {\n        await workflowStore.orchestrate(graph);\n\n        message.destroy(loadingKey);\n        message.success(t(\"common.text.operation_succeeded\"));\n      } catch (err) {\n        console.error(err);\n        message.destroy(loadingKey);\n        notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n      }\n    });\n  };\n\n  const handleExportClick = () => {\n    graphExportModal.open({ data: workflow.graphDraft! });\n  };\n\n  return (\n    <div className=\"size-full\">\n      <Card\n        className=\"size-full overflow-hidden\"\n        styles={{\n          body: {\n            position: \"relative\",\n            height: \"100%\",\n            padding: 0,\n          },\n        }}\n      >\n        <WorkflowDesigner\n          ref={designerRef}\n          defaultLayout={\n            globalAppSettings.defaultWorkflowLayout === \"horizontal\"\n              ? FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT\n              : globalAppSettings.defaultWorkflowLayout === \"vertical\"\n                ? FlowLayoutDefault.VERTICAL_FIXED_LAYOUT\n                : void 0\n          }\n          onDocumentChange={handleDesignerDocumentChange}\n          onNodeClick={(_, node) => designerNodeDrawer.open(node)}\n        >\n          <div className=\"absolute top-8 z-10 w-full px-4\">\n            <div className=\"container\">\n              <div className=\"flex items-center justify-end gap-4\">\n                <div className=\"flex flex-1 items-center justify-end gap-4 overflow-hidden\">\n                  <div className=\"flex-1 overflow-hidden\">\n                    <Show when={workflow.hasDraft!}>\n                      <Alert showIcon title={<div className=\"truncate\">{t(\"workflow.detail.design.unpublished_draft.alert\")}</div>} type=\"warning\" />\n                    </Show>\n                  </div>\n                  <Space.Compact\n                    style={{\n                      backgroundColor: themeToken.colorBgContainer,\n                      borderRadius: themeToken.borderRadius,\n                    }}\n                  >\n                    <Button disabled={workflowPublishDisabled} ghost type=\"primary\" onClick={handlePublishClick}>\n                      {t(\"workflow.detail.design.action.publish.button\")}\n                    </Button>\n                    <Dropdown\n                      menu={{\n                        items: [\n                          {\n                            key: \"rollback\",\n                            disabled: workflowRollbackDisabled,\n                            label: t(\"workflow.detail.design.action.rollback.button\"),\n                            icon: <IconArrowBackUp size=\"1.25em\" />,\n                            onClick: handleRollbackClick,\n                          },\n                          {\n                            type: \"divider\",\n                          },\n                          {\n                            key: \"import\",\n                            label: t(\"workflow.detail.design.action.import.button\"),\n                            icon: <IconTransferIn size=\"1.25em\" />,\n                            onClick: handleImportClick,\n                          },\n                          {\n                            key: \"export\",\n                            label: t(\"workflow.detail.design.action.export.button\"),\n                            icon: <IconTransferOut size=\"1.25em\" />,\n                            onClick: handleExportClick,\n                          },\n                        ],\n                      }}\n                      trigger={[\"click\"]}\n                    >\n                      <Button icon={<IconDots size=\"1.25em\" />} />\n                    </Dropdown>\n                  </Space.Compact>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <div className=\"absolute bottom-8 z-10 w-full px-4\">\n            <div className=\"container\">\n              <div className=\"flex justify-end\">\n                <WorkflowToolbar\n                  style={{\n                    backgroundColor: themeToken.colorBgContainer,\n                    borderRadius: themeToken.borderRadius,\n                  }}\n                />\n              </div>\n            </div>\n          </div>\n\n          {!!designerError && (\n            <div className=\"absolute top-1/2 left-1/2 z-10 w-full -translate-1/2 px-4\">\n              <Result status=\"warning\" title=\"Data corruption!\" subTitle={`Error: ${unwrapErrMsg(designerError)}`} />\n            </div>\n          )}\n\n          <WorkflowNodeDrawer {...designerNodeDrawerProps} />\n        </WorkflowDesigner>\n\n        <WorkflowGraphImportModal {...graphImportModalProps} />\n        <WorkflowGraphExportModal {...graphExportModalProps} />\n      </Card>\n    </div>\n  );\n};\n\nexport default WorkflowDetailDesign;\n"
  },
  {
    "path": "ui/src/pages/workflows/WorkflowDetailRuns.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { IconBox, IconBrowserShare, IconCertificate, IconDots, IconHistory, IconPlayerPause, IconTrash } from \"@tabler/icons-react\";\nimport { useRequest } from \"ahooks\";\nimport { App, Button, Dropdown, Skeleton, Table, type TableProps, theme } from \"antd\";\nimport dayjs from \"dayjs\";\nimport { ClientResponseError } from \"pocketbase\";\n\nimport { cancelRun as cancelWorkflowRun } from \"@/api/workflows\";\nimport Empty from \"@/components/Empty\";\nimport Show from \"@/components/Show\";\nimport Tips from \"@/components/Tips\";\nimport WorkflowRunDetailDrawer from \"@/components/workflow/WorkflowRunDetailDrawer\";\nimport WorkflowStatus from \"@/components/workflow/WorkflowStatus\";\nimport { WORKFLOW_TRIGGERS } from \"@/domain/workflow\";\nimport { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from \"@/domain/workflowRun\";\nimport { useAppSettings, useZustandShallowSelector } from \"@/hooks\";\nimport { get as getWorkflowRun, list as listWorkflowRuns, remove as removeWorkflowRun, subscribe as subscribeWorkflowRun } from \"@/repository/workflowRun\";\nimport { useWorkflowStore } from \"@/stores/workflow\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nconst WorkflowDetailRuns = () => {\n  const { t } = useTranslation();\n\n  const { token: themeToken } = theme.useToken();\n\n  const { modal, notification } = App.useApp();\n\n  const { appSettings: globalAppSettings } = useAppSettings();\n\n  const { workflow } = useWorkflowStore(useZustandShallowSelector([\"workflow\"]));\n\n  const [page, setPage] = useState<number>(1);\n  const [pageSize, setPageSize] = useState<number>(globalAppSettings.defaultPerPage!);\n\n  const [tableData, setTableData] = useState<WorkflowRunModel[]>([]);\n  const [tableTotal, setTableTotal] = useState<number>(0);\n  const [tableSelectedRowKeys, setTableSelectedRowKeys] = useState<string[]>([]);\n  const tableColumns: TableProps<WorkflowRunModel>[\"columns\"] = [\n    {\n      key: \"id\",\n      title: \"ID\",\n      width: 160,\n      render: (_, record) => <span className=\"font-mono\">{record.id}</span>,\n    },\n    {\n      key: \"status\",\n      title: t(\"workflow_run.props.status\"),\n      render: (_, record) => {\n        return <WorkflowStatus type=\"filled\" value={record.status} />;\n      },\n    },\n    {\n      key: \"trigger\",\n      title: t(\"workflow_run.props.trigger\"),\n      ellipsis: true,\n      render: (_, record) => {\n        if (record.trigger === WORKFLOW_TRIGGERS.SCHEDULED) {\n          return t(\"workflow_run.props.trigger.scheduled\");\n        } else if (record.trigger === WORKFLOW_TRIGGERS.MANUAL) {\n          return t(\"workflow_run.props.trigger.manual\");\n        }\n\n        return <></>;\n      },\n    },\n    {\n      key: \"startedAt\",\n      title: t(\"workflow_run.props.started_at\"),\n      ellipsis: true,\n      render: (_, record) => {\n        if (record.startedAt) {\n          return dayjs(record.startedAt).format(\"YYYY-MM-DD HH:mm:ss\");\n        }\n\n        return <></>;\n      },\n    },\n    {\n      key: \"endedAt\",\n      title: t(\"workflow_run.props.ended_at\"),\n      ellipsis: true,\n      render: (_, record) => {\n        if (record.endedAt) {\n          return dayjs(record.endedAt).format(\"YYYY-MM-DD HH:mm:ss\");\n        }\n\n        return <></>;\n      },\n    },\n    {\n      key: \"artifacts\",\n      title: t(\"workflow_run.props.artifacts\"),\n      width: 160,\n      render: (_, record) => {\n        if (record.outputs && record.outputs.length > 0) {\n          const keys = new Set<string>();\n          const icons: React.ReactNode[] = [];\n\n          for (const output of record.outputs) {\n            if (output.type === \"ref\" && output.value?.split(\"#\")?.at(0) === \"certificate\") {\n              const KEY = \"certificate\";\n              if (keys.has(KEY)) continue;\n\n              keys.add(KEY);\n              icons.push(<IconCertificate key={KEY} size=\"1.25em\" />);\n            } else {\n              const KEY = \"other\";\n              if (keys.has(KEY)) continue;\n\n              keys.add(KEY);\n              icons.push(<IconBox key={KEY} size=\"1.25em\" />);\n            }\n          }\n\n          return <div className=\"flex items-center gap-2\">{icons}</div>;\n        }\n\n        return <></>;\n      },\n    },\n    {\n      key: \"$action\",\n      align: \"end\",\n      fixed: \"right\",\n      width: 64,\n      render: (_, record) => {\n        const cancelDisabled = !([WORKFLOW_RUN_STATUSES.PENDING, WORKFLOW_RUN_STATUSES.PROCESSING] as string[]).includes(record.status);\n        const deleteDisabled = !cancelDisabled;\n\n        return (\n          <Dropdown\n            menu={{\n              items: [\n                {\n                  key: \"view\",\n                  label: t(\"workflow_run.action.view.menu\"),\n                  icon: (\n                    <span className=\"anticon scale-125\">\n                      <IconBrowserShare size=\"1em\" />\n                    </span>\n                  ),\n                  onClick: () => {\n                    handleRecordDetailClick(record);\n                  },\n                },\n                {\n                  key: \"cancel\",\n                  label: <span style={{ color: cancelDisabled ? void 0 : \"var(--color-warning)\" }}>{t(\"workflow_run.action.cancel.menu\")}</span>,\n                  icon: (\n                    <span className=\"anticon scale-125\">\n                      <IconPlayerPause size=\"1em\" color={cancelDisabled ? void 0 : \"var(--color-warning)\"} />\n                    </span>\n                  ),\n                  disabled: cancelDisabled,\n                  onClick: () => {\n                    handleRecordCancelClick(record);\n                  },\n                },\n                {\n                  type: \"divider\",\n                },\n                {\n                  key: \"delete\",\n                  label: t(\"workflow_run.action.delete.menu\"),\n                  icon: (\n                    <span className=\"anticon scale-125\">\n                      <IconTrash size=\"1em\" />\n                    </span>\n                  ),\n                  danger: true,\n                  disabled: deleteDisabled,\n                  onClick: () => {\n                    handleRecordDeleteClick(record);\n                  },\n                },\n              ],\n            }}\n            trigger={[\"click\"]}\n          >\n            <Button icon={<IconDots size=\"1.25em\" />} type=\"text\" />\n          </Dropdown>\n        );\n      },\n      onCell: () => {\n        return {\n          onClick: (e) => {\n            e.stopPropagation();\n          },\n        };\n      },\n    },\n  ];\n  const tableRowSelection: TableProps<WorkflowRunModel>[\"rowSelection\"] = {\n    fixed: true,\n    selectedRowKeys: tableSelectedRowKeys,\n    renderCell(checked, _, index, node) {\n      if (!checked) {\n        return (\n          <div className=\"group/selection\">\n            <div className=\"group-hover/selection:hidden\">{(page - 1) * pageSize + index + 1}</div>\n            <div className=\"hidden group-hover/selection:block\">{node}</div>\n          </div>\n        );\n      }\n      return node;\n    },\n    onCell: () => {\n      return {\n        onClick: (e) => {\n          e.stopPropagation();\n        },\n      };\n    },\n    onChange: (keys) => {\n      setTableSelectedRowKeys(keys as string[]);\n    },\n  };\n\n  const {\n    loading,\n    error: loadError,\n    run: refreshData,\n  } = useRequest(\n    () => {\n      return listWorkflowRuns({\n        workflowId: workflow.id,\n        page: page,\n        perPage: pageSize,\n      });\n    },\n    {\n      refreshDeps: [workflow.id, page, pageSize],\n      onSuccess: (res) => {\n        setTableData(res.items);\n        setTableTotal(res.totalItems);\n        setTableSelectedRowKeys([]);\n      },\n      onError: (err) => {\n        if (err instanceof ClientResponseError && err.isAbort) {\n          return;\n        }\n\n        console.error(err);\n        notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n\n        throw err;\n      },\n    }\n  );\n\n  useEffect(() => {\n    const unsubscribers = new Map<string, () => void>();\n    const items = tableData.filter((e) => e.status === WORKFLOW_RUN_STATUSES.PENDING || e.status === WORKFLOW_RUN_STATUSES.PROCESSING);\n    for (const item of items) {\n      subscribeWorkflowRun(item.id, (cb) => {\n        setTableData((prev) => {\n          const index = prev.findIndex((e) => e.id === item.id);\n          if (index !== -1) {\n            prev[index] = cb.record;\n          }\n          return [...prev];\n        });\n\n        if (cb.record.status !== WORKFLOW_RUN_STATUSES.PENDING && cb.record.status !== WORKFLOW_RUN_STATUSES.PROCESSING) {\n          unsubscribers.get(cb.record.id)?.();\n          unsubscribers.delete(cb.record.id);\n        }\n      }).then((unsubscriber) => {\n        unsubscribers.set(item.id, unsubscriber);\n      });\n    }\n\n    return () => {\n      for (const unsubscriber of unsubscribers.values()) {\n        unsubscriber();\n      }\n      unsubscribers.clear();\n    };\n  }, [tableData]);\n\n  const handlePaginationChange = (page: number, pageSize: number) => {\n    setPage(page);\n    setPageSize(pageSize);\n  };\n\n  const { drawerProps: detailDrawerProps, ...detailDrawer } = WorkflowRunDetailDrawer.useDrawer();\n\n  const handleRecordDetailClick = (workflowRun: WorkflowRunModel) => {\n    const drawer = detailDrawer.open({ data: workflowRun, loading: true });\n    getWorkflowRun(workflowRun.id).then((data) => {\n      drawer.safeUpdate({ data, loading: false });\n    });\n  };\n\n  const handleRecordCancelClick = (workflowRun: WorkflowRunModel) => {\n    modal.confirm({\n      title: t(\"workflow_run.action.cancel.modal.title\"),\n      content: t(\"workflow_run.action.cancel.modal.content\"),\n      onOk: async () => {\n        try {\n          const resp = await cancelWorkflowRun(workflow.id, workflowRun.id);\n          if (resp) {\n            refreshData();\n          }\n        } catch (err) {\n          console.error(err);\n          notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n        }\n      },\n    });\n  };\n\n  const handleRecordDeleteClick = (workflowRun: WorkflowRunModel) => {\n    modal.confirm({\n      title: <span className=\"text-error\">{t(\"workflow_run.action.delete.modal.title\", { name: `#${workflowRun.id}` })}</span>,\n      content: <span dangerouslySetInnerHTML={{ __html: t(\"workflow_run.action.delete.modal.content\") }} />,\n      icon: (\n        <span className=\"anticon\" role=\"img\">\n          <IconTrash className=\"text-error\" size=\"1em\" />\n        </span>\n      ),\n      okText: t(\"common.button.confirm\"),\n      okButtonProps: { danger: true },\n      onOk: async () => {\n        try {\n          const resp = await removeWorkflowRun(workflowRun);\n          if (resp) {\n            setTableData((prev) => prev.filter((item) => item.id !== workflowRun.id));\n            refreshData();\n          }\n        } catch (err) {\n          console.error(err);\n          notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n        }\n      },\n    });\n  };\n\n  const handleBatchDeleteClick = () => {\n    let records = tableData.filter((item) => tableSelectedRowKeys.includes(item.id));\n    if (records.length === 0) {\n      return;\n    }\n\n    modal.confirm({\n      title: <span className=\"text-error\">{t(\"workflow_run.action.batch_delete.modal.title\")}</span>,\n      content: <span dangerouslySetInnerHTML={{ __html: t(\"workflow_run.action.batch_delete.modal.content\", { count: records.length }) }} />,\n      icon: (\n        <span className=\"anticon\" role=\"img\">\n          <IconTrash className=\"text-error\" size=\"1em\" />\n        </span>\n      ),\n      okText: t(\"common.button.confirm\"),\n      okButtonProps: { danger: true },\n      onOk: async () => {\n        // 未结束的记录不允许删除\n        records = records.filter((record) => !([WORKFLOW_RUN_STATUSES.PENDING, WORKFLOW_RUN_STATUSES.PROCESSING] as string[]).includes(record.status));\n        try {\n          const resp = await removeWorkflowRun(records);\n          if (resp) {\n            setTableData((prev) => prev.filter((item) => !records.some((record) => record.id === item.id)));\n            setTableTotal((prev) => prev - records.length);\n            refreshData();\n          }\n        } catch (err) {\n          console.error(err);\n          notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n        }\n      },\n    });\n  };\n\n  return (\n    <div className=\"container\">\n      <div className=\"pt-9\">\n        <Tips className=\"mb-4\" message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_run.deletion.alert\") }}></span>} />\n        <Tips className=\"mb-4\" message={<span dangerouslySetInnerHTML={{ __html: t(\"workflow_run.cancellation.alert\") }}></span>} />\n\n        <div className=\"relative\">\n          <Table<WorkflowRunModel>\n            columns={tableColumns}\n            dataSource={tableData}\n            loading={loading}\n            locale={{\n              emptyText: loading ? (\n                <Skeleton />\n              ) : (\n                <Empty\n                  className=\"py-24\"\n                  title={loadError ? t(\"common.text.nodata_failed\") : t(\"workflow_run.nodata.title\")}\n                  description={loadError ? unwrapErrMsg(loadError) : t(\"workflow_run.nodata.description\")}\n                  icon={<IconHistory size={24} />}\n                />\n              ),\n            }}\n            pagination={{\n              current: page,\n              pageSize: pageSize,\n              total: tableTotal,\n              showSizeChanger: true,\n              onChange: handlePaginationChange,\n              onShowSizeChange: handlePaginationChange,\n            }}\n            rowClassName=\"cursor-pointer\"\n            rowKey={(record) => record.id}\n            rowSelection={tableRowSelection}\n            scroll={{ x: \"max(100%, 960px)\" }}\n            onRow={(record) => ({\n              onClick: () => {\n                handleRecordDetailClick(record);\n              },\n            })}\n          />\n\n          <Show when={tableSelectedRowKeys.length > 0}>\n            <div\n              className=\"absolute top-0 right-0 left-[32px] z-10 h-[54px]\"\n              style={{\n                left: \"32px\", // Match the width of the table row selection checkbox\n                height: \"54px\", // Match the height of the table header\n                background: themeToken.Table?.headerBg ?? themeToken.colorBgElevated,\n              }}\n            >\n              <div className=\"flex size-full items-center justify-end gap-x-2 overflow-hidden px-4 py-2\">\n                <Button danger ghost onClick={handleBatchDeleteClick}>\n                  {t(\"common.button.delete\")}\n                </Button>\n              </div>\n            </div>\n          </Show>\n        </div>\n\n        <WorkflowRunDetailDrawer {...detailDrawerProps} />\n      </div>\n    </div>\n  );\n};\n\nexport default WorkflowDetailRuns;\n"
  },
  {
    "path": "ui/src/pages/workflows/WorkflowList.tsx",
    "content": "import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useNavigate, useSearchParams } from \"react-router-dom\";\nimport { IconCirclePlus, IconCopy, IconDots, IconEdit, IconHierarchy3, IconPlayerPlay, IconPlus, IconReload, IconTrash } from \"@tabler/icons-react\";\nimport { useControllableValue, useRequest } from \"ahooks\";\nimport { App, Button, Dropdown, Form, Input, Segmented, Skeleton, Switch, Table, type TableProps, Typography, theme } from \"antd\";\nimport { createSchemaFieldRule } from \"antd-zod\";\nimport dayjs from \"dayjs\";\nimport { ClientResponseError } from \"pocketbase\";\nimport { z } from \"zod\";\n\nimport { startRun as startWorkflowRun } from \"@/api/workflows\";\nimport DrawerForm from \"@/components/DrawerForm\";\nimport Empty from \"@/components/Empty\";\nimport Show from \"@/components/Show\";\nimport WorkflowStatus from \"@/components/workflow/WorkflowStatus\";\nimport { WORKFLOW_TRIGGERS, type WorkflowModel, duplicateNodes } from \"@/domain/workflow\";\nimport { useAntdForm, useAppSettings } from \"@/hooks\";\nimport { get as getWorkflow, list as listWorkflows, remove as removeWorkflow, save as saveWorkflow } from \"@/repository/workflow\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nconst WorkflowList = () => {\n  const navigate = useNavigate();\n  const [searchParams, setSearchParams] = useSearchParams();\n\n  const { t } = useTranslation();\n\n  const { token: themeToken } = theme.useToken();\n\n  const { message, modal, notification } = App.useApp();\n\n  const { appSettings: globalAppSettings } = useAppSettings();\n\n  const [filters, setFilters] = useState<Record<string, unknown>>(() => {\n    return {\n      keyword: searchParams.get(\"keyword\"),\n      state: searchParams.get(\"state\"),\n    };\n  });\n  const [sorter, setSorter] = useState<ArrayElement<Parameters<NonNullable<TableProps<WorkflowModel>[\"onChange\"]>>[2]>>(() => {\n    return {};\n  });\n  const [page, setPage] = useState<number>(() => parseInt(+searchParams.get(\"page\")! + \"\") || 1);\n  const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get(\"perPage\")! + \"\") || globalAppSettings.defaultPerPage!);\n\n  const [tableData, setTableData] = useState<WorkflowModel[]>([]);\n  const [tableTotal, setTableTotal] = useState<number>(0);\n  const [tableSelectedRowKeys, setTableSelectedRowKeys] = useState<string[]>([]);\n  const tableColumns: TableProps<WorkflowModel>[\"columns\"] = [\n    {\n      key: \"name\",\n      title: t(\"workflow.props.name\"),\n      render: (_, record) => (\n        <div className=\"flex max-w-full flex-col gap-1 truncate\">\n          <Typography.Text ellipsis>{record.name || \"\\u00A0\"}</Typography.Text>\n          <Typography.Text ellipsis type=\"secondary\">\n            {record.description || \"\\u00A0\"}\n          </Typography.Text>\n        </div>\n      ),\n    },\n    {\n      key: \"trigger\",\n      title: t(\"workflow.props.trigger\"),\n      render: (_, record) => {\n        const trigger = record.trigger;\n        if (!trigger) {\n          return \"-\";\n        } else if (trigger === WORKFLOW_TRIGGERS.MANUAL) {\n          return <Typography.Text>{t(\"workflow.props.trigger.manual\")}</Typography.Text>;\n        } else if (trigger === WORKFLOW_TRIGGERS.SCHEDULED) {\n          return (\n            <div className=\"flex max-w-full flex-col gap-1\">\n              <Typography.Text>{t(\"workflow.props.trigger.scheduled\")}</Typography.Text>\n              <Typography.Text type=\"secondary\">{record.triggerCron || \"\\u00A0\"}</Typography.Text>\n            </div>\n          );\n        }\n      },\n    },\n    {\n      key: \"state\",\n      title: t(\"workflow.props.state\"),\n      defaultFilteredValue: searchParams.has(\"state\") ? [searchParams.get(\"state\") as string] : void 0,\n      render: (_, record) => {\n        return (\n          <Switch\n            checked={record.enabled}\n            onChange={() => {\n              handleRecordActiveChange(record);\n            }}\n          />\n        );\n      },\n      onCell: () => {\n        return {\n          onClick: (e) => {\n            e.stopPropagation();\n          },\n        };\n      },\n    },\n    {\n      key: \"lastRun\",\n      title: t(\"workflow.props.last_run_at\"),\n      sorter: true,\n      sortOrder: sorter.columnKey === \"lastRun\" ? sorter.order : void 0,\n      render: (_, record) => {\n        const { lastRunStatus, lastRunTime } = record;\n        if (!lastRunStatus) {\n          return <></>;\n        } else {\n          return (\n            <WorkflowStatus type=\"filled\" value={lastRunStatus}>\n              {lastRunTime ? dayjs(lastRunTime).format(\"YYYY-MM-DD HH:mm:ss\") : \"\"}\n            </WorkflowStatus>\n          );\n        }\n      },\n    },\n    {\n      key: \"createdAt\",\n      title: t(\"workflow.props.created_at\"),\n      ellipsis: true,\n      render: (_, record) => {\n        return dayjs(record.created!).format(\"YYYY-MM-DD HH:mm:ss\");\n      },\n    },\n    {\n      key: \"$action\",\n      align: \"end\",\n      fixed: \"right\",\n      width: 64,\n      render: (_, record) => (\n        <Dropdown\n          menu={{\n            items: [\n              {\n                key: \"modify\",\n                label: t(\"workflow.action.modify.menu\"),\n                icon: (\n                  <span className=\"anticon scale-125\">\n                    <IconEdit size=\"1em\" />\n                  </span>\n                ),\n                onClick: () => {\n                  handleRecordDetailClick(record);\n                },\n              },\n              {\n                key: \"duplicate\",\n                label: t(\"workflow.action.duplicate.menu\"),\n                icon: (\n                  <span className=\"anticon scale-125\">\n                    <IconCopy size=\"1em\" />\n                  </span>\n                ),\n                onClick: () => {\n                  handleRecordDuplicateClick(record);\n                },\n              },\n              {\n                key: \"execute\",\n                label: t(\"workflow.action.execute.menu\"),\n                icon: (\n                  <span className=\"anticon scale-125\">\n                    <IconPlayerPlay size=\"1em\" />\n                  </span>\n                ),\n                disabled: !record.hasContent,\n                onClick: () => {\n                  handleRecordExecuteClick(record);\n                },\n              },\n              {\n                type: \"divider\",\n              },\n              {\n                key: \"delete\",\n                label: t(\"workflow.action.delete.menu\"),\n                danger: true,\n                icon: (\n                  <span className=\"anticon scale-125\">\n                    <IconTrash size=\"1em\" />\n                  </span>\n                ),\n                onClick: () => {\n                  handleRecordDeleteClick(record);\n                },\n              },\n            ],\n          }}\n          trigger={[\"click\"]}\n        >\n          <Button icon={<IconDots size=\"1.25em\" />} type=\"text\" />\n        </Dropdown>\n      ),\n      onCell: () => {\n        return {\n          onClick: (e) => {\n            e.stopPropagation();\n          },\n        };\n      },\n    },\n  ];\n  const tableRowSelection: TableProps<WorkflowModel>[\"rowSelection\"] = {\n    fixed: true,\n    selectedRowKeys: tableSelectedRowKeys,\n    renderCell(checked, _, index, node) {\n      if (!checked) {\n        return (\n          <div className=\"group/selection\">\n            <div className=\"group-hover/selection:hidden\">{(page - 1) * pageSize + index + 1}</div>\n            <div className=\"hidden group-hover/selection:block\">{node}</div>\n          </div>\n        );\n      }\n      return node;\n    },\n    onCell: () => {\n      return {\n        onClick: (e) => {\n          e.stopPropagation();\n        },\n      };\n    },\n    onChange: (keys) => {\n      setTableSelectedRowKeys(keys as string[]);\n    },\n  };\n\n  const {\n    loading,\n    error: loadError,\n    run: refreshData,\n  } = useRequest(\n    () => {\n      const { columnKey: sorterKey, order: sorterOrder } = sorter;\n      let sort: string | undefined;\n      sort = sorterKey === \"lastRun\" ? \"lastRunTime\" : void 0;\n      sort = sort && (sorterOrder === \"ascend\" ? `${sort}` : sorterOrder === \"descend\" ? `-${sort}` : void 0);\n\n      return listWorkflows({\n        keyword: filters[\"keyword\"] as string,\n        enabled: (filters[\"state\"] as string) === \"enabled\" ? true : (filters[\"state\"] as string) === \"disabled\" ? false : void 0,\n        sort: sort,\n        page: page,\n        perPage: pageSize,\n        expand: true,\n      });\n    },\n    {\n      refreshDeps: [filters, sorter, page, pageSize],\n      onBefore: () => {\n        setSearchParams((prev) => {\n          if (filters[\"keyword\"]) {\n            prev.set(\"keyword\", filters[\"keyword\"] as string);\n          } else {\n            prev.delete(\"keyword\");\n          }\n\n          if (filters[\"state\"]) {\n            prev.set(\"state\", filters[\"state\"] as string);\n          } else {\n            prev.delete(\"state\");\n          }\n\n          prev.set(\"page\", page.toString());\n          prev.set(\"perPage\", pageSize.toString());\n\n          return prev;\n        });\n      },\n      onSuccess: (res) => {\n        setTableData(res.items);\n        setTableTotal(res.totalItems);\n        setTableSelectedRowKeys([]);\n      },\n      onError: (err) => {\n        if (err instanceof ClientResponseError && err.isAbort) {\n          return;\n        }\n\n        console.error(err);\n        notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n\n        throw err;\n      },\n    }\n  );\n\n  const [duplicateDrawerOpen, setDuplicateDrawerOpen] = useState(false);\n  const [duplicateDrawerData, setDuplicateDrawerData] = useState<Nullish<WorkflowModel>>();\n\n  const handleSearch = (value: string) => {\n    setFilters((prev) => ({ ...prev, keyword: value.trim() }));\n    setPage(1);\n  };\n\n  const handleReloadClick = () => {\n    if (loading) return;\n\n    refreshData();\n  };\n\n  const handlePaginationChange = (page: number, pageSize: number) => {\n    setPage(page);\n    setPageSize(pageSize);\n  };\n\n  const handleCreateClick = () => {\n    navigate(\"/workflows/new\");\n  };\n\n  const handleRecordDetailClick = (workflow: WorkflowModel) => {\n    navigate(`/workflows/${workflow.id}`);\n  };\n\n  const handleRecordActiveChange = async (workflow: WorkflowModel) => {\n    try {\n      if (!workflow.enabled && !workflow.hasContent) {\n        message.warning(t(\"workflow.action.enable.errmsg.unpublished\"));\n        return;\n      }\n\n      const resp = await saveWorkflow({\n        id: workflow.id,\n        enabled: !tableData.find((item) => item.id === workflow.id)?.enabled,\n      });\n      if (resp) {\n        setTableData((prev) => {\n          return prev.map((item) => {\n            if (item.id === workflow.id) {\n              item.enabled = resp.enabled;\n              item.updated = resp.updated;\n            }\n            return item;\n          });\n        });\n      }\n    } catch (err) {\n      console.error(err);\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n    }\n  };\n\n  const handleRecordExecuteClick = async (workflow: WorkflowModel) => {\n    try {\n      await startWorkflowRun(workflow.id);\n\n      message.info(t(\"workflow.action.execute.prompt\"));\n    } catch (err) {\n      console.error(err);\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n    }\n  };\n\n  const handleRecordDuplicateClick = async (workflow: WorkflowModel) => {\n    const data = await getWorkflow(workflow.id);\n    setDuplicateDrawerOpen(true);\n    setDuplicateDrawerData({\n      name: `${data.name}-copy`,\n      description: data.description,\n      trigger: data.trigger,\n      triggerCron: data.triggerCron,\n      graphDraft: { nodes: duplicateNodes(data.graphDraft?.nodes ?? [], { withCopySuffix: false }) },\n      hasDraft: true,\n    });\n  };\n\n  const handleRecordDeleteClick = (workflow: WorkflowModel) => {\n    modal.confirm({\n      title: <span className=\"text-error\">{t(\"workflow.action.delete.modal.title\", { name: workflow.name })}</span>,\n      content: <span dangerouslySetInnerHTML={{ __html: t(\"workflow.action.delete.modal.content\") }} />,\n      icon: (\n        <span className=\"anticon\" role=\"img\">\n          <IconTrash className=\"text-error\" size=\"1em\" />\n        </span>\n      ),\n      okText: t(\"common.button.confirm\"),\n      okButtonProps: { danger: true },\n      onOk: async () => {\n        try {\n          const resp = await removeWorkflow(workflow);\n          if (resp) {\n            setTableData((prev) => prev.filter((item) => item.id !== workflow.id));\n            refreshData();\n          }\n        } catch (err) {\n          console.error(err);\n          notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n        }\n      },\n    });\n  };\n\n  const handleBatchDeleteClick = () => {\n    const records = tableData.filter((item) => tableSelectedRowKeys.includes(item.id));\n    if (records.length === 0) {\n      return;\n    }\n\n    modal.confirm({\n      title: <span className=\"text-error\">{t(\"workflow.action.batch_delete.modal.title\")}</span>,\n      content: <span dangerouslySetInnerHTML={{ __html: t(\"workflow.action.batch_delete.modal.content\", { count: records.length }) }} />,\n      icon: (\n        <span className=\"anticon\" role=\"img\">\n          <IconTrash className=\"text-error\" size=\"1em\" />\n        </span>\n      ),\n      okText: t(\"common.button.confirm\"),\n      okButtonProps: { danger: true },\n      onOk: async () => {\n        try {\n          const resp = await removeWorkflow(records);\n          if (resp) {\n            setTableData((prev) => prev.filter((item) => !records.some((record) => record.id === item.id)));\n            setTableTotal((prev) => prev - records.length);\n            refreshData();\n          }\n        } catch (err) {\n          console.error(err);\n          notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n        }\n      },\n    });\n  };\n\n  const handleDuplicateDrawerSubmit = async (values: Nullish<WorkflowModel>) => {\n    try {\n      const resp = await saveWorkflow(values);\n      if (resp) {\n        refreshData();\n      }\n    } catch (err) {\n      console.error(err);\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n    }\n  };\n\n  return (\n    <div className=\"px-6 py-4\">\n      <div className=\"container\">\n        <h1>{t(\"workflow.page.title\")}</h1>\n        <p className=\"text-base text-gray-500\">{t(\"workflow.page.subtitle\")}</p>\n      </div>\n\n      <div className=\"container\">\n        <div className=\"flex items-center justify-between gap-x-2 gap-y-3 not-md:flex-col-reverse not-md:items-start not-md:justify-normal\">\n          <div className=\"flex w-full flex-1 items-center gap-x-2 md:max-w-200\">\n            <div>\n              <Segmented\n                options={[\n                  { label: <span className=\"text-sm\">{t(\"workflow.props.state.filter.all\")}</span>, value: \"\" },\n                  { label: <span className=\"text-sm\">{t(\"workflow.props.state.filter.enabled\")}</span>, value: \"enabled\" },\n                  { label: <span className=\"text-sm\">{t(\"workflow.props.state.filter.disabled\")}</span>, value: \"disabled\" },\n                ]}\n                size=\"large\"\n                value={(filters[\"state\"] as string) || \"\"}\n                onChange={(value) => {\n                  setPage(1);\n                  setFilters((prev) => ({ ...prev, state: value }));\n                }}\n              />\n            </div>\n            <div className=\"flex-1\">\n              <Input.Search\n                className=\"text-sm placeholder:text-sm\"\n                allowClear\n                defaultValue={filters[\"keyword\"] as string}\n                placeholder={t(\"workflow.search.placeholder\")}\n                size=\"large\"\n                onSearch={handleSearch}\n              />\n            </div>\n            <div>\n              <Button icon={<IconReload size=\"1.25em\" />} size=\"large\" onClick={handleReloadClick} />\n            </div>\n          </div>\n          <div>\n            <Button className=\"text-sm\" icon={<IconPlus size=\"1.25em\" />} size=\"large\" type=\"primary\" onClick={handleCreateClick}>\n              {t(\"workflow.action.create.button\")}\n            </Button>\n          </div>\n        </div>\n\n        <div className=\"relative mt-4\">\n          <Table<WorkflowModel>\n            columns={tableColumns}\n            dataSource={tableData}\n            loading={loading}\n            locale={{\n              emptyText: loading ? (\n                <Skeleton />\n              ) : (\n                <Empty\n                  className=\"py-24\"\n                  title={loadError ? t(\"common.text.nodata_failed\") : t(\"workflow.nodata.title\")}\n                  description={loadError ? unwrapErrMsg(loadError) : t(\"workflow.nodata.description\")}\n                  icon={<IconHierarchy3 size={24} />}\n                  extra={\n                    loadError ? (\n                      <Button ghost icon={<IconReload size=\"1.25em\" />} type=\"primary\" onClick={handleReloadClick}>\n                        {t(\"common.button.reload\")}\n                      </Button>\n                    ) : (\n                      <Button icon={<IconCirclePlus size=\"1.25em\" />} type=\"primary\" onClick={handleCreateClick}>\n                        {t(\"workflow.nodata.button\")}\n                      </Button>\n                    )\n                  }\n                />\n              ),\n            }}\n            pagination={{\n              current: page,\n              pageSize: pageSize,\n              total: tableTotal,\n              showSizeChanger: true,\n              onChange: handlePaginationChange,\n              onShowSizeChange: handlePaginationChange,\n            }}\n            rowClassName=\"cursor-pointer\"\n            rowKey={(record) => record.id}\n            rowSelection={tableRowSelection}\n            scroll={{ x: \"max(100%, 960px)\" }}\n            onChange={(_, __, sorter) => {\n              setSorter(Array.isArray(sorter) ? sorter[0] : sorter);\n            }}\n            onRow={(record) => ({\n              onClick: () => {\n                handleRecordDetailClick(record);\n              },\n            })}\n          />\n\n          <Show when={tableSelectedRowKeys.length > 0}>\n            <div\n              className=\"absolute top-0 right-0 left-[32px] z-10 h-[54px]\"\n              style={{\n                left: \"32px\", // Match the width of the table row selection checkbox\n                height: \"54px\", // Match the height of the table header\n                background: themeToken.Table?.headerBg ?? themeToken.colorBgElevated,\n              }}\n            >\n              <div className=\"flex size-full items-center justify-end gap-x-2 overflow-hidden px-4 py-2\">\n                <Button danger ghost onClick={handleBatchDeleteClick}>\n                  {t(\"common.button.delete\")}\n                </Button>\n              </div>\n            </div>\n          </Show>\n\n          <InternalDuplicateDrawer\n            data={duplicateDrawerData}\n            open={duplicateDrawerOpen}\n            afterClose={() => setDuplicateDrawerOpen(false)}\n            onOpenChange={(open) => setDuplicateDrawerOpen(open)}\n            onSubmit={handleDuplicateDrawerSubmit}\n          />\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst InternalDuplicateDrawer = ({\n  data,\n  trigger,\n  onSubmit,\n  ...props\n}: {\n  afterClose?: () => void;\n  data?: Nullish<WorkflowModel>;\n  open: boolean;\n  trigger?: React.ReactNode;\n  onOpenChange?: (open: boolean) => void;\n  onSubmit?: (record: Nullish<WorkflowModel>) => void;\n}) => {\n  const { t } = useTranslation();\n\n  const [open, setOpen] = useControllableValue<boolean>(props, {\n    valuePropName: \"open\",\n    defaultValuePropName: \"defaultOpen\",\n    trigger: \"onOpenChange\",\n  });\n\n  const afterClose = () => {\n    formInst.resetFields();\n    props.afterClose?.();\n  };\n\n  const formSchema = z.object({\n    name: z.string().nonempty(t(\"workflow.detail.baseinfo.name.placeholder\")),\n    description: z.string().nullish(),\n  });\n  const formRule = createSchemaFieldRule(formSchema);\n  const { form: formInst, formProps } = useAntdForm<z.infer<typeof formSchema>>({\n    name: \"viewWorkflowList_InternalDuplicateDrawer\",\n    initialValues: data,\n  });\n\n  const handleFormFinish = async (values: z.infer<typeof formSchema>) => {\n    await onSubmit?.(values);\n    setOpen(false);\n  };\n\n  return (\n    <DrawerForm\n      {...formProps}\n      clearOnDestroy\n      drawerProps={{ autoFocus: true, destroyOnHidden: true, size: \"large\", afterOpenChange: (open) => !open && afterClose?.() }}\n      form={formInst}\n      layout=\"vertical\"\n      okText={t(\"common.button.create\")}\n      open={open}\n      preserve={false}\n      title={t(\"workflow.action.create.modal.title\")}\n      trigger={trigger}\n      validateTrigger=\"onSubmit\"\n      onFinish={handleFormFinish}\n      onOpenChange={props.onOpenChange}\n    >\n      <Form.Item name=\"name\" label={t(\"workflow.detail.baseinfo.name.label\")} rules={[formRule]}>\n        <Input maxLength={100} placeholder={t(\"workflow.detail.baseinfo.name.placeholder\")} />\n      </Form.Item>\n\n      <Form.Item name=\"description\" label={t(\"workflow.detail.baseinfo.description.label\")} rules={[formRule]}>\n        <Input placeholder={t(\"workflow.detail.baseinfo.description.placeholder\")} />\n      </Form.Item>\n    </DrawerForm>\n  );\n};\n\nexport default WorkflowList;\n"
  },
  {
    "path": "ui/src/pages/workflows/WorkflowNew.tsx",
    "content": "import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useNavigate } from \"react-router-dom\";\nimport { IconArrowRight, IconSquarePlus2, IconUpload } from \"@tabler/icons-react\";\nimport { App, Button, Card, Spin, Typography } from \"antd\";\n\nimport Show from \"@/components/Show\";\nimport WorkflowGraphImportModal from \"@/components/workflow/WorkflowGraphImportModal\";\nimport {\n  WORKFLOW_NODE_TYPES,\n  type WorkflowModel,\n  type WorkflowNodeConfigForBizDeploy,\n  type WorkflowNodeConfigForBizNotify,\n  type WorkflowNodeConfigForBranchBlock,\n  newNode,\n} from \"@/domain/workflow\";\nimport { save as saveWorkflow } from \"@/repository/workflow\";\nimport { unwrapErrMsg } from \"@/utils/error\";\n\nconst TEMPLATE_KEY_BLANK = \"blank\" as const;\nconst TEMPLATE_KEY_STANDARD = \"standard\" as const;\nconst TEMPLATE_KEY_CERTTEST = \"certtest\" as const;\ntype TemplateKeys = typeof TEMPLATE_KEY_BLANK | typeof TEMPLATE_KEY_CERTTEST | typeof TEMPLATE_KEY_STANDARD;\n\nconst WorkflowNew = () => {\n  const navigate = useNavigate();\n\n  const { i18n, t } = useTranslation();\n\n  const { notification } = App.useApp();\n\n  const templates = [\n    {\n      key: TEMPLATE_KEY_STANDARD,\n      name: t(\"workflow.new.templates.template.standard.title\"),\n      description: t(\"workflow.new.templates.template.standard.description\"),\n      image: \"/imgs/workflow/tpl-standard.png\",\n    },\n    {\n      key: TEMPLATE_KEY_CERTTEST,\n      name: t(\"workflow.new.templates.template.certtest.title\"),\n      description: t(\"workflow.new.templates.template.certtest.description\"),\n      image: \"/imgs/workflow/tpl-certtest.png\",\n    },\n  ];\n  const [templateSelectKey, setTemplateSelectKey] = useState<TemplateKeys>();\n  const [templatePending, setTemplatePending] = useState(false);\n\n  const renderTemplateCard = ({ key, name, description, image }: { key: TemplateKeys; name: string; description: string; image: string }) => {\n    return (\n      <Card\n        key={key}\n        className=\"group/card size-full\"\n        cover={<img className=\"min-h-[120px] object-contain\" src={image} />}\n        hoverable\n        onClick={() => handleTemplateClick(key)}\n      >\n        <div className=\"flex w-full items-center gap-4\">\n          <Card.Meta\n            className=\"grow\"\n            title={\n              <div className=\"flex w-full items-center justify-between gap-4 overflow-hidden transition-colors group-hover/card:text-primary\">\n                <div className=\"flex-1 truncate\">{name}</div>\n                <Show when={templatePending} fallback={<IconArrowRight className=\"opacity-0 transition-opacity group-hover/card:opacity-100\" size=\"1.25em\" />}>\n                  <Spin spinning={templateSelectKey === key} />\n                </Show>\n              </div>\n            }\n            description={description}\n          />\n        </div>\n      </Card>\n    );\n  };\n\n  const { modalProps: workflowImportModalProps, ...workflowImportModal } = WorkflowGraphImportModal.useModal();\n\n  const handleTemplateClick = async (key: TemplateKeys) => {\n    if (templatePending) return;\n\n    setTemplateSelectKey(key);\n    setTemplatePending(true);\n\n    try {\n      let workflow = {} as WorkflowModel;\n      workflow.name = t(\"workflow.new.templates.default_name\");\n      workflow.description = t(\"workflow.new.templates.default_description\");\n      workflow.graphDraft = { nodes: [] };\n      workflow.hasDraft = true;\n\n      switch (key) {\n        case TEMPLATE_KEY_BLANK:\n          {\n            const startNode = newNode(WORKFLOW_NODE_TYPES.START, { i18n: i18n });\n            const endNode = newNode(WORKFLOW_NODE_TYPES.END, { i18n: i18n });\n\n            workflow.graphDraft!.nodes = [startNode, endNode];\n          }\n          break;\n\n        case TEMPLATE_KEY_STANDARD:\n          {\n            const startNode = newNode(WORKFLOW_NODE_TYPES.START, { i18n: i18n });\n            const tryCatchNode = newNode(WORKFLOW_NODE_TYPES.TRYCATCH, { i18n: i18n });\n            const applyNode = newNode(WORKFLOW_NODE_TYPES.BIZ_APPLY, { i18n: i18n });\n            const deployNode = newNode(WORKFLOW_NODE_TYPES.BIZ_DEPLOY, { i18n: i18n });\n            const notifyOnFailureNode = newNode(WORKFLOW_NODE_TYPES.BIZ_NOTIFY, { i18n: i18n });\n            const endNode = newNode(WORKFLOW_NODE_TYPES.END, { i18n: i18n });\n\n            deployNode.data.config = {\n              ...deployNode.data.config,\n              certificateOutputNodeId: applyNode.id,\n            } as WorkflowNodeConfigForBizDeploy;\n\n            notifyOnFailureNode.data.config = {\n              ...notifyOnFailureNode.data.config,\n              subject: \"[Certimate] Workflow Failure Alert!\",\n              message: 'Your workflow \"{{ $workflow.name }}\" run has failed. Please check the details.',\n            } as WorkflowNodeConfigForBizNotify;\n\n            tryCatchNode.blocks!.at(0)!.blocks ??= [];\n            tryCatchNode.blocks!.at(0)!.blocks!.push(applyNode, deployNode);\n            tryCatchNode.blocks!.at(1)!.blocks ??= [];\n            tryCatchNode.blocks!.at(1)!.blocks!.unshift(notifyOnFailureNode);\n\n            workflow.graphDraft!.nodes = [startNode, tryCatchNode, endNode];\n          }\n          break;\n\n        case TEMPLATE_KEY_CERTTEST:\n          {\n            const startNode = newNode(WORKFLOW_NODE_TYPES.START, { i18n: i18n });\n            const tryCatchNode = newNode(WORKFLOW_NODE_TYPES.TRYCATCH, { i18n: i18n });\n            const monitorNode = newNode(WORKFLOW_NODE_TYPES.BIZ_MONITOR, { i18n: i18n });\n            const conditionNode = newNode(WORKFLOW_NODE_TYPES.CONDITION, { i18n: i18n });\n            const notifyOnExpiringSoonNode = newNode(WORKFLOW_NODE_TYPES.BIZ_NOTIFY, { i18n: i18n });\n            const notifyOnExpiredNode = newNode(WORKFLOW_NODE_TYPES.BIZ_NOTIFY, { i18n: i18n });\n            const notifyOnFailureNode = newNode(WORKFLOW_NODE_TYPES.BIZ_NOTIFY, { i18n: i18n });\n            const endNode = newNode(WORKFLOW_NODE_TYPES.END, { i18n: i18n });\n\n            notifyOnExpiringSoonNode.data.config = {\n              ...notifyOnExpiringSoonNode.data.config,\n              subject: \"[Certimate] Certificate Expiry Alert!\",\n              message:\n                \"The certificate which you are monitoring will be expiring soon. Please pay attention to your website. \\r\\nDomains: {{ $certificate.subjectAltNames }} \\r\\nExpiration: {{ $certificate.notAfter }}({{ $certificate.daysLeft }} days left)\",\n            } as WorkflowNodeConfigForBizNotify;\n\n            notifyOnExpiredNode.data.config = {\n              ...notifyOnExpiredNode.data.config,\n              subject: \"[Certimate] Certificate Expiry Alert!\",\n              message:\n                \"The certificate which you are monitoring has already expired. Please pay attention to your website. \\r\\nDomains: {{ $certificate.subjectAltNames }} \\r\\nExpiration: {{ $certificate.notAfter }}\",\n            } as WorkflowNodeConfigForBizNotify;\n\n            notifyOnFailureNode.data.config = {\n              ...notifyOnFailureNode.data.config,\n              subject: \"[Certimate] Workflow Failure Alert!\",\n              message: 'Your workflow \"{{ $workflow.name }}\" run has failed. Please check the details.',\n            } as WorkflowNodeConfigForBizNotify;\n\n            tryCatchNode.blocks!.at(0)!.blocks ??= [];\n            tryCatchNode.blocks!.at(0)!.blocks!.push(monitorNode, conditionNode);\n            tryCatchNode.blocks!.at(1)!.blocks ??= [];\n            tryCatchNode.blocks!.at(1)!.blocks!.unshift(notifyOnFailureNode);\n\n            conditionNode.blocks!.at(0)!.data.name = t(\"workflow_node.condition.default_name.template_certtest_on_expiring_soon\");\n            conditionNode.blocks!.at(0)!.data.config = {\n              ...conditionNode.blocks!.at(0)!.data.config,\n              expression: {\n                left: {\n                  left: {\n                    selector: {\n                      id: monitorNode.id,\n                      name: \"certificate.validity\",\n                      type: \"boolean\",\n                    },\n                    type: \"var\",\n                  },\n                  operator: \"eq\",\n                  right: {\n                    type: \"const\",\n                    value: \"true\",\n                    valueType: \"boolean\",\n                  },\n                  type: \"comparison\",\n                },\n                operator: \"and\",\n                right: {\n                  left: {\n                    selector: {\n                      id: monitorNode.id,\n                      name: \"certificate.daysLeft\",\n                      type: \"number\",\n                    },\n                    type: \"var\",\n                  },\n                  operator: \"lte\",\n                  right: {\n                    type: \"const\",\n                    value: \"30\",\n                    valueType: \"number\",\n                  },\n                  type: \"comparison\",\n                },\n                type: \"logical\",\n              },\n            } as WorkflowNodeConfigForBranchBlock;\n            conditionNode.blocks!.at(0)!.blocks ??= [];\n            conditionNode.blocks!.at(0)!.blocks!.push(notifyOnExpiringSoonNode);\n            conditionNode.blocks!.at(1)!.data.name = t(\"workflow_node.condition.default_name.template_certtest_on_expired\");\n            conditionNode.blocks!.at(1)!.data.config = {\n              ...conditionNode.blocks!.at(1)!.data.config,\n              expression: {\n                left: {\n                  selector: {\n                    id: monitorNode.id,\n                    name: \"certificate.validity\",\n                    type: \"boolean\",\n                  },\n                  type: \"var\",\n                },\n                operator: \"eq\",\n                right: {\n                  type: \"const\",\n                  value: \"false\",\n                  valueType: \"boolean\",\n                },\n                type: \"comparison\",\n              },\n            } as WorkflowNodeConfigForBranchBlock;\n            conditionNode.blocks!.at(1)!.blocks ??= [];\n            conditionNode.blocks!.at(1)!.blocks!.push(notifyOnExpiredNode);\n\n            workflow.graphDraft!.nodes = [startNode, tryCatchNode, endNode];\n          }\n          break;\n\n        default:\n          throw \"Invalid value of `templateSelectKey`\";\n      }\n\n      workflow = await saveWorkflow(workflow);\n      navigate(`/workflows/${workflow.id}`, { replace: true });\n    } catch (err) {\n      notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n\n      throw err;\n    } finally {\n      setTemplatePending(false);\n      setTemplateSelectKey(void 0);\n    }\n  };\n\n  const handleImportClick = async () => {\n    if (templatePending) return;\n\n    workflowImportModal.open().then(async (graph) => {\n      setTemplatePending(true);\n\n      try {\n        let workflow = {} as WorkflowModel;\n        workflow.name = t(\"workflow.new.templates.default_name\");\n        workflow.description = t(\"workflow.new.templates.default_description\");\n        workflow.graphDraft = graph;\n        workflow.hasDraft = true;\n        workflow = await saveWorkflow(workflow);\n        navigate(`/workflows/${workflow.id}`, { replace: true });\n      } catch (err) {\n        notification.error({ title: t(\"common.text.request_error\"), description: unwrapErrMsg(err) });\n\n        throw err;\n      } finally {\n        setTemplatePending(false);\n      }\n    });\n  };\n\n  return (\n    <div className=\"px-6 py-4\">\n      <div className=\"container\">\n        <h1>{t(\"workflow.new.title\")}</h1>\n        <p className=\"text-base text-gray-500\">{t(\"workflow.new.subtitle\")}</p>\n      </div>\n\n      <div className=\"container\">\n        <div className=\"my-1.5\">\n          <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4\">\n            <Card className=\"size-full\" styles={{ body: { padding: \"1rem 0.5rem\" } }} variant=\"borderless\">\n              <div className=\"flex flex-col gap-3\">\n                <Button block icon={<IconSquarePlus2 size=\"1.25em\" />} type=\"text\" onClick={() => handleTemplateClick(TEMPLATE_KEY_BLANK)}>\n                  <div className=\"w-full text-left\">{t(\"workflow.new.button.create\")}</div>\n                </Button>\n                <Button block icon={<IconUpload size=\"1.25em\" />} type=\"text\" onClick={handleImportClick}>\n                  <div className=\"w-full text-left\">{t(\"workflow.new.button.import\")}</div>\n                </Button>\n              </div>\n            </Card>\n\n            <WorkflowGraphImportModal {...workflowImportModalProps} />\n          </div>\n        </div>\n\n        <div className=\"mt-8\">\n          <h3>{t(\"workflow.new.templates.title\")}</h3>\n          <Typography.Text type=\"secondary\">\n            <div className=\"mb-4\">{t(\"workflow.new.templates.subtitle\")}</div>\n          </Typography.Text>\n\n          <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4\">\n            {templates.map((template) => renderTemplateCard(template))}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default WorkflowNew;\n"
  },
  {
    "path": "ui/src/repository/_pocketbase.ts",
    "content": "import PocketBase from \"pocketbase\";\n\nlet pb: PocketBase;\nexport const getPocketBase = () => {\n  if (pb) return pb;\n  pb = new PocketBase(\"/\");\n  pb.afterSend = (res, data) => {\n    if ((res.status === 401 || res.status === 403) && pb.authStore?.isValid) {\n      pb.authStore.clear();\n      location.reload();\n    }\n    return data;\n  };\n  return pb;\n};\n\nexport const COLLECTION_NAME_ADMIN = \"_superusers\";\nexport const COLLECTION_NAME_ACCESS = \"access\";\nexport const COLLECTION_NAME_CERTIFICATE = \"certificate\";\nexport const COLLECTION_NAME_SETTINGS = \"settings\";\nexport const COLLECTION_NAME_WORKFLOW = \"workflow\";\nexport const COLLECTION_NAME_WORKFLOW_RUN = \"workflow_run\";\nexport const COLLECTION_NAME_WORKFLOW_OUTPUT = \"workflow_output\";\nexport const COLLECTION_NAME_WORKFLOW_LOG = \"workflow_logs\";\n"
  },
  {
    "path": "ui/src/repository/access.ts",
    "content": "import dayjs from \"dayjs\";\n\nimport { type AccessModel } from \"@/domain/access\";\nimport { COLLECTION_NAME_ACCESS, getPocketBase } from \"./_pocketbase\";\n\nconst _commonFields = [\"id\", \"name\", \"provider\", \"reserve\", \"created\", \"updated\", \"deleted\"];\n\nexport const list = async () => {\n  const list = await getPocketBase()\n    .collection(COLLECTION_NAME_ACCESS)\n    .getFullList<AccessModel>({\n      batch: 65535,\n      fields: [..._commonFields].join(\",\"),\n      filter: \"deleted=null\",\n      sort: \"-created\",\n      requestKey: null,\n    });\n  return {\n    totalItems: list.length,\n    items: list,\n  };\n};\n\nexport const get = async (id: string) => {\n  return await getPocketBase().collection(COLLECTION_NAME_ACCESS).getOne<AccessModel>(id, {\n    requestKey: null,\n  });\n};\n\nexport const save = async (record: MaybeModelRecord<AccessModel>) => {\n  if (record.id) {\n    return await getPocketBase().collection(COLLECTION_NAME_ACCESS).update<AccessModel>(record.id, record);\n  }\n\n  return await getPocketBase().collection(COLLECTION_NAME_ACCESS).create<AccessModel>(record);\n};\n\nexport const remove = async (record: MaybeModelRecordWithId<AccessModel> | MaybeModelRecordWithId<AccessModel>[]) => {\n  const pb = getPocketBase();\n\n  const deletedAt = dayjs.utc().format(\"YYYY-MM-DD HH:mm:ss\");\n\n  if (Array.isArray(record)) {\n    const batch = pb.createBatch();\n    for (const item of record) {\n      batch.collection(COLLECTION_NAME_ACCESS).update(item.id, { deleted: deletedAt });\n    }\n    const res = await batch.send();\n    return res.every((e) => e.status >= 200 && e.status < 400);\n  } else {\n    await pb.collection(COLLECTION_NAME_ACCESS).update<AccessModel>(record.id!, { deleted: deletedAt });\n    return true;\n  }\n};\n"
  },
  {
    "path": "ui/src/repository/admin.ts",
    "content": "﻿import { COLLECTION_NAME_ADMIN, getPocketBase } from \"./_pocketbase\";\n\nexport const authWithPassword = (username: string, password: string) => {\n  return getPocketBase().collection(COLLECTION_NAME_ADMIN).authWithPassword(username, password);\n};\n\nexport const getAuthStore = () => {\n  return getPocketBase().authStore;\n};\n\nexport const save = (data: { email: string } | { password: string; passwordConfirm: string }) => {\n  return getPocketBase()\n    .collection(COLLECTION_NAME_ADMIN)\n    .update(getAuthStore().record?.id || \"\", data);\n};\n"
  },
  {
    "path": "ui/src/repository/certificate.ts",
    "content": "import dayjs from \"dayjs\";\n\nimport { type CertificateModel } from \"@/domain/certificate\";\nimport { COLLECTION_NAME_CERTIFICATE, getPocketBase } from \"./_pocketbase\";\n\nconst _commonFields = [\n  \"id\",\n  \"source\",\n  \"subjectAltNames\",\n  \"serialNumber\",\n  \"issuerOrg\",\n  \"keyAlgorithm\",\n  \"validityNotBefore\",\n  \"validityNotAfter\",\n  \"validityInterval\",\n  \"isRenewed\",\n  \"isRevoked\",\n  \"workflowRef\",\n  \"created\",\n  \"updated\",\n  \"deleted\",\n];\nconst _expandFields = [\"expand.workflowRef.id\", \"expand.workflowRef.name\", \"expand.workflowRef.description\"];\n\nexport const list = async ({\n  keyword,\n  state,\n  stateThreshold,\n  sort = \"-created\",\n  page = 1,\n  perPage = 10,\n}: {\n  keyword?: string;\n  state?: \"expiringSoon\" | \"expired\";\n  stateThreshold?: number;\n  sort?: string;\n  page?: number;\n  perPage?: number;\n}) => {\n  const pb = getPocketBase();\n\n  const filters: string[] = [\"deleted=null\"];\n  if (keyword) {\n    filters.push(pb.filter(\"(id={:keyword} || serialNumber={:keyword} || subjectAltNames~{:keyword})\", { keyword: keyword }));\n  }\n  if (state === \"expiringSoon\") {\n    filters.push(pb.filter(\"validityNotAfter<={:expiredAt}\", { expiredAt: dayjs().add(stateThreshold!, \"d\").toDate() }));\n    filters.push(pb.filter(\"validityNotAfter>@now\"));\n    filters.push(pb.filter(\"isRevoked=0\"));\n  } else if (state === \"expired\") {\n    filters.push(pb.filter(\"validityNotAfter<=@now\"));\n  }\n\n  return pb.collection(COLLECTION_NAME_CERTIFICATE).getList<CertificateModel>(page, perPage, {\n    expand: [\"workflowRef\"].join(\",\"),\n    fields: [..._commonFields, ..._expandFields].join(\",\"),\n    filter: filters.join(\" && \"),\n    sort: sort || \"-created\",\n    requestKey: null,\n  });\n};\n\nexport const listByWorkflowRunId = async (workflowRunId: string) => {\n  const pb = getPocketBase();\n\n  const list = await pb.collection(COLLECTION_NAME_CERTIFICATE).getFullList<CertificateModel>({\n    batch: 65535,\n    fields: [..._commonFields, ..._expandFields, \"certificate\", \"privateKey\"].join(\",\"),\n    filter: pb.filter(\"workflowRunRef={:workflowRunId}\", { workflowRunId }),\n    sort: \"created\",\n    requestKey: null,\n  });\n\n  return {\n    totalItems: list.length,\n    items: list,\n  };\n};\n\nexport const get = async (id: string) => {\n  return await getPocketBase()\n    .collection(COLLECTION_NAME_CERTIFICATE)\n    .getOne<CertificateModel>(id, {\n      expand: [\"workflowRef\"].join(\",\"),\n      fields: [\"*\", ..._expandFields].join(\",\"),\n      requestKey: null,\n    });\n};\n\nexport const remove = async (record: MaybeModelRecordWithId<CertificateModel> | MaybeModelRecordWithId<CertificateModel>[]) => {\n  const pb = getPocketBase();\n\n  const deletedAt = dayjs.utc().format(\"YYYY-MM-DD HH:mm:ss\");\n\n  if (Array.isArray(record)) {\n    const batch = pb.createBatch();\n    for (const item of record) {\n      batch.collection(COLLECTION_NAME_CERTIFICATE).update(item.id, { deleted: deletedAt });\n    }\n    const res = await batch.send();\n    return res.every((e) => e.status >= 200 && e.status < 400);\n  } else {\n    await pb.collection(COLLECTION_NAME_CERTIFICATE).update<CertificateModel>(record.id!, { deleted: deletedAt });\n    return true;\n  }\n};\n"
  },
  {
    "path": "ui/src/repository/settings.ts",
    "content": "import { ClientResponseError } from \"pocketbase\";\n\nimport { CA_PROVIDERS } from \"@/domain/provider\";\nimport {\n  type EmailsSettingsContent,\n  type NotifyTemplateContent,\n  type PersistenceSettingsContent,\n  SETTINGS_NAMES,\n  type SSLProviderSettingsContent,\n  type ScriptTemplateContent,\n  type SettingsModel,\n  type SettingsNames,\n} from \"@/domain/settings\";\n\nimport { COLLECTION_NAME_SETTINGS, getPocketBase } from \"./_pocketbase\";\n\ninterface SettingsContentMap {\n  [SETTINGS_NAMES.EMAILS]: EmailsSettingsContent;\n  [SETTINGS_NAMES.NOTIFY_TEMPLATE]: NotifyTemplateContent;\n  [SETTINGS_NAMES.SCRIPT_TEMPLATE]: ScriptTemplateContent;\n  [SETTINGS_NAMES.SSL_PROVIDER]: SSLProviderSettingsContent;\n  [SETTINGS_NAMES.PERSISTENCE]: PersistenceSettingsContent;\n}\n\nexport const get = async <K extends SettingsNames | string, T extends NonNullable<unknown>>(\n  name: K\n): Promise<K extends keyof SettingsContentMap ? SettingsModel<SettingsContentMap[K]> : SettingsModel<T>> => {\n  let resp: K extends keyof SettingsContentMap ? SettingsModel<SettingsContentMap[K]> : SettingsModel<T>;\n  try {\n    resp = await getPocketBase().collection(COLLECTION_NAME_SETTINGS).getFirstListItem<typeof resp>(`name='${name}'`, {\n      requestKey: null,\n    });\n    return resp;\n  } catch (err) {\n    if (err instanceof ClientResponseError && err.status === 404) {\n      resp = {\n        name: name,\n        content: {},\n      } as unknown as typeof resp;\n    } else {\n      throw err;\n    }\n  }\n\n  // 兜底设置一些默认值（需确保与后端默认值保持一致），防止视图层空指针\n  switch (name) {\n    case SETTINGS_NAMES.EMAILS:\n      {\n        resp.content ??= {};\n        (resp.content as EmailsSettingsContent).emails ??= [];\n      }\n      break;\n\n    case SETTINGS_NAMES.NOTIFY_TEMPLATE:\n      {\n        resp.content ??= {};\n        (resp.content as NotifyTemplateContent).templates ??= [];\n      }\n      break;\n\n    case SETTINGS_NAMES.SCRIPT_TEMPLATE:\n      {\n        resp.content ??= {};\n        (resp.content as ScriptTemplateContent).templates ??= [];\n      }\n      break;\n\n    case SETTINGS_NAMES.SSL_PROVIDER:\n      {\n        resp.content ??= {};\n        (resp.content as SSLProviderSettingsContent).provider ??= CA_PROVIDERS.LETSENCRYPT;\n      }\n      break;\n\n    case SETTINGS_NAMES.PERSISTENCE:\n      {\n        resp.content ??= {};\n        (resp.content as PersistenceSettingsContent).certificatesWarningDaysBeforeExpire ??= 21;\n        (resp.content as PersistenceSettingsContent).certificatesRetentionMaxDays ??= 0;\n        (resp.content as PersistenceSettingsContent).workflowRunsRetentionMaxDays ??= 0;\n      }\n      break;\n  }\n\n  return resp;\n};\n\nexport const save = async <T extends NonNullable<unknown>>(record: MaybeModelRecordWithId<SettingsModel<T>>) => {\n  if (record.id) {\n    return await getPocketBase().collection(COLLECTION_NAME_SETTINGS).update<SettingsModel<T>>(record.id, record);\n  }\n\n  return await getPocketBase().collection(COLLECTION_NAME_SETTINGS).create<SettingsModel<T>>(record);\n};\n"
  },
  {
    "path": "ui/src/repository/system.ts",
    "content": "﻿import { getPocketBase } from \"./_pocketbase\";\n\nexport const listCronJobs = () => {\n  return getPocketBase()\n    .crons.getFullList({\n      requestKey: null,\n    })\n    .then((res) => {\n      const jobs = res\n        .filter((job) => !job.id.startsWith(\"__pb\"))\n        .map((job) => {\n          return {\n            id: job.id,\n            cron: job.expression,\n          };\n        });\n      return {\n        items: jobs,\n      };\n    });\n};\n\nexport type ListLogsRequest = {\n  page?: number;\n  perPage?: number;\n};\n\nexport const listLogs = (request: ListLogsRequest) => {\n  const page = request.page || 1;\n  const perPage = request.perPage || 10;\n\n  return getPocketBase()\n    .logs.getList(page, perPage, {\n      filter: 'data.type!=\"request\"',\n      sort: \"-@rowid\",\n      skipTotal: true,\n      requestKey: null,\n    })\n    .then((res) => {\n      return {\n        items: res.items,\n      };\n    });\n};\n"
  },
  {
    "path": "ui/src/repository/workflow.ts",
    "content": "import { type RecordSubscription } from \"pocketbase\";\n\nimport { type WorkflowModel } from \"@/domain/workflow\";\nimport { COLLECTION_NAME_WORKFLOW, getPocketBase } from \"./_pocketbase\";\n\nconst _commonFields = [\n  \"id\",\n  \"name\",\n  \"description\",\n  \"trigger\",\n  \"triggerCron\",\n  \"enabled\",\n  \"hasDraft\",\n  \"hasContent\",\n  \"lastRunRef\",\n  \"lastRunStatus\",\n  \"lastRunTime\",\n  \"created\",\n  \"updated\",\n  \"deleted\",\n];\nconst _expandFields = [\n  \"expand.lastRunRef.id\",\n  \"expand.lastRunRef.status\",\n  \"expand.lastRunRef.trigger\",\n  \"expand.lastRunRef.startedAt\",\n  \"expand.lastRunRef.endedAt\",\n  \"expand.lastRunRef.error\",\n];\n\nexport const list = async ({\n  keyword,\n  enabled,\n  sort = \"-created\",\n  page = 1,\n  perPage = 10,\n  expand = false,\n}: {\n  keyword?: string;\n  enabled?: boolean;\n  sort?: string;\n  page?: number;\n  perPage?: number;\n  expand?: boolean;\n}) => {\n  const pb = getPocketBase();\n\n  const filters: string[] = [];\n  if (keyword) {\n    filters.push(pb.filter(\"(id={:keyword} || name~{:keyword})\", { keyword: keyword }));\n  }\n  if (enabled != null) {\n    filters.push(pb.filter(\"enabled={:enabled}\", { enabled: enabled }));\n  }\n\n  return await pb.collection(COLLECTION_NAME_WORKFLOW).getList<WorkflowModel>(page, perPage, {\n    expand: expand ? [\"lastRunRef\"].join(\",\") : void 0,\n    fields: [..._commonFields, ..._expandFields].join(\",\"),\n    filter: filters.join(\" && \"),\n    sort: sort || \"-created\",\n    requestKey: null,\n  });\n};\n\nexport const get = async (id: string) => {\n  return await getPocketBase()\n    .collection(COLLECTION_NAME_WORKFLOW)\n    .getOne<WorkflowModel>(id, {\n      expand: [\"lastRunRef\"].join(\",\"),\n      fields: [\"*\", ..._expandFields].join(\",\"),\n      requestKey: null,\n    });\n};\n\nexport const save = async (record: MaybeModelRecord<WorkflowModel>) => {\n  if (record.id) {\n    return await getPocketBase()\n      .collection(COLLECTION_NAME_WORKFLOW)\n      .update<WorkflowModel>(record.id as string, record);\n  }\n\n  return await getPocketBase().collection(COLLECTION_NAME_WORKFLOW).create<WorkflowModel>(record);\n};\n\nexport const remove = async (record: MaybeModelRecordWithId<WorkflowModel> | MaybeModelRecordWithId<WorkflowModel>[]) => {\n  const pb = getPocketBase();\n\n  if (Array.isArray(record)) {\n    const batch = pb.createBatch();\n    for (const item of record) {\n      batch.collection(COLLECTION_NAME_WORKFLOW).delete(item.id);\n    }\n    const res = await batch.send();\n    return res.every((e) => e.status >= 200 && e.status < 400);\n  } else {\n    return await pb.collection(COLLECTION_NAME_WORKFLOW).delete(record.id);\n  }\n};\n\nexport const subscribe = async (id: string, cb: (e: RecordSubscription<WorkflowModel>) => void) => {\n  return getPocketBase().collection(COLLECTION_NAME_WORKFLOW).subscribe(id, cb);\n};\n\nexport const unsubscribe = async (id: string) => {\n  return getPocketBase().collection(COLLECTION_NAME_WORKFLOW).unsubscribe(id);\n};\n"
  },
  {
    "path": "ui/src/repository/workflowLog.ts",
    "content": "﻿import { type WorkflowLogModel } from \"@/domain/workflowLog\";\n\nimport { COLLECTION_NAME_WORKFLOW_LOG, getPocketBase } from \"./_pocketbase\";\n\nexport const listByWorkflowRunId = async (workflowRunId: string) => {\n  const pb = getPocketBase();\n\n  const list = await pb.collection(COLLECTION_NAME_WORKFLOW_LOG).getFullList<WorkflowLogModel>({\n    batch: 65535,\n    filter: pb.filter(\"runRef={:workflowRunId}\", { workflowRunId }),\n    sort: \"timestamp\",\n    requestKey: null,\n  });\n\n  return {\n    totalItems: list.length,\n    items: list,\n  };\n};\n"
  },
  {
    "path": "ui/src/repository/workflowRun.ts",
    "content": "﻿import { type RecordSubscription } from \"pocketbase\";\n\nimport { type WorkflowRunModel } from \"@/domain/workflowRun\";\n\nimport { COLLECTION_NAME_WORKFLOW_OUTPUT, COLLECTION_NAME_WORKFLOW_RUN, getPocketBase } from \"./_pocketbase\";\n\nconst _commonFields = [\"id\", \"status\", \"trigger\", \"startedAt\", \"endedAt\", \"error\", \"created\", \"updated\", \"deleted\"];\nconst _expandFields = [\"expand.workflowRef.id\", \"expand.workflowRef.name\", \"expand.workflowRef.description\"];\n\nexport const list = async ({\n  workflowId,\n  page = 1,\n  perPage = 10,\n  expand = false,\n}: {\n  workflowId?: string;\n  page?: number;\n  perPage?: number;\n  expand?: boolean;\n}) => {\n  const pb = getPocketBase();\n\n  const filters: string[] = [];\n  if (workflowId) {\n    filters.push(pb.filter(\"workflowRef={:workflowId}\", { workflowId: workflowId }));\n  }\n\n  const list = await pb.collection(COLLECTION_NAME_WORKFLOW_RUN).getList<WorkflowRunModel>(page, perPage, {\n    expand: expand ? [\"workflowRef\"].join(\",\") : void 0,\n    fields: [..._commonFields, ..._expandFields].join(\",\"),\n    filter: filters.join(\" && \"),\n    sort: \"-created\",\n    requestKey: null,\n  });\n  await enrichOutputs(list.items);\n  return list;\n};\n\nexport const get = async (id: string) => {\n  const record = await getPocketBase()\n    .collection(COLLECTION_NAME_WORKFLOW_RUN)\n    .getOne<WorkflowRunModel>(id, {\n      expand: [\"workflowRef\"].join(\",\"),\n      fields: [\"*\", ..._expandFields].join(\",\"),\n      requestKey: null,\n    });\n  await enrichOutputs(record);\n  return record;\n};\n\nexport const remove = async (record: MaybeModelRecordWithId<WorkflowRunModel> | MaybeModelRecordWithId<WorkflowRunModel>[]) => {\n  const pb = getPocketBase();\n\n  if (Array.isArray(record)) {\n    const batch = pb.createBatch();\n    for (const item of record) {\n      batch.collection(COLLECTION_NAME_WORKFLOW_RUN).delete(item.id);\n    }\n    const res = await batch.send();\n    return res.every((e) => e.status >= 200 && e.status < 400);\n  } else {\n    await pb.collection(COLLECTION_NAME_WORKFLOW_RUN).delete(record.id!);\n    return true;\n  }\n};\n\nexport const subscribe = async (id: string, cb: (e: RecordSubscription<WorkflowRunModel>) => void) => {\n  return getPocketBase().collection(COLLECTION_NAME_WORKFLOW_RUN).subscribe(id, cb);\n};\n\nexport const unsubscribe = async (id: string) => {\n  return getPocketBase().collection(COLLECTION_NAME_WORKFLOW_RUN).unsubscribe(id);\n};\n\nconst enrichOutputs = async (records: WorkflowRunModel | WorkflowRunModel[]) => {\n  if (!Array.isArray(records)) {\n    records = [records];\n  }\n\n  const runIds = Array.from(new Set(records.map((e) => e.id)));\n  if (runIds.length === 0) {\n    return;\n  }\n\n  const pb = getPocketBase();\n  const list = await pb.collection(COLLECTION_NAME_WORKFLOW_OUTPUT).getFullList({\n    batch: 65535,\n    fields: [\"id\", \"runRef\", \"outputs\"].join(\",\"),\n    filter: \"(\" + runIds.map((runId) => pb.filter(\"runRef={:runId}\", { runId })).join(\" || \") + \") && outputs!=null\",\n    sort: \"created\",\n    requestKey: null,\n  });\n\n  for (const record of records) {\n    const outputs = list\n      .filter((e) => e.runRef === record.id)\n      .map((e) => e.outputs)\n      .flat();\n    record.outputs = outputs;\n  }\n};\n"
  },
  {
    "path": "ui/src/routers/index.tsx",
    "content": "﻿import { createHashRouter } from \"react-router-dom\";\n\nimport AccessList from \"@/pages/accesses/AccessList\";\nimport AccessNew from \"@/pages/accesses/AccessNew\";\nimport AuthLayout from \"@/pages/AuthLayout\";\nimport CertificateList from \"@/pages/certificates/CertificateList\";\nimport ConsoleLayout from \"@/pages/ConsoleLayout\";\nimport Dashboard from \"@/pages/dashboard/Dashboard\";\nimport ErrorLayout from \"@/pages/ErrorLayout\";\nimport Login from \"@/pages/login/Login\";\nimport PresetList from \"@/pages/presets/PresetList\";\nimport Settings from \"@/pages/settings/Settings\";\nimport SettingsAbout from \"@/pages/settings/SettingsAbout\";\nimport SettingsAccount from \"@/pages/settings/SettingsAccount\";\nimport SettingsAppearance from \"@/pages/settings/SettingsAppearance\";\nimport SettingsDiagnostics from \"@/pages/settings/SettingsDiagnostics\";\nimport SettingsPersistence from \"@/pages/settings/SettingsPersistence\";\nimport SettingsSSLProvider from \"@/pages/settings/SettingsSSLProvider\";\nimport WorkflowDetail from \"@/pages/workflows/WorkflowDetail\";\nimport WorkflowDetailDesign from \"@/pages/workflows/WorkflowDetailDesign\";\nimport WorkflowDetailRuns from \"@/pages/workflows/WorkflowDetailRuns\";\nimport WorkflowList from \"@/pages/workflows/WorkflowList\";\nimport WorkflowNew from \"@/pages/workflows/WorkflowNew\";\n\nexport const router = createHashRouter([\n  {\n    path: \"/\",\n    element: <ConsoleLayout />,\n    children: [\n      {\n        path: \"/\",\n        element: <Dashboard />,\n      },\n      {\n        path: \"/accesses\",\n        element: <AccessList />,\n      },\n      {\n        path: \"/accesses/new\",\n        element: <AccessNew />,\n      },\n      {\n        path: \"/certificates\",\n        element: <CertificateList />,\n      },\n      {\n        path: \"/workflows\",\n        element: <WorkflowList />,\n      },\n      {\n        path: \"/workflows/new\",\n        element: <WorkflowNew />,\n      },\n      {\n        path: \"/workflows/:id\",\n        element: <WorkflowDetail />,\n        children: [\n          {\n            path: \"/workflows/:id/design\",\n            element: <WorkflowDetailDesign />,\n          },\n          {\n            path: \"/workflows/:id/runs\",\n            element: <WorkflowDetailRuns />,\n          },\n        ],\n      },\n      {\n        path: \"/presets\",\n        element: <PresetList />,\n      },\n      {\n        path: \"/settings\",\n        element: <Settings />,\n        children: [\n          {\n            path: \"/settings/account\",\n            element: <SettingsAccount />,\n          },\n          {\n            path: \"/settings/appearance\",\n            element: <SettingsAppearance />,\n          },\n          {\n            path: \"/settings/ssl-provider\",\n            element: <SettingsSSLProvider />,\n          },\n          {\n            path: \"/settings/persistence\",\n            element: <SettingsPersistence />,\n          },\n          {\n            path: \"/settings/diagnostics\",\n            element: <SettingsDiagnostics />,\n          },\n          {\n            path: \"/settings/about\",\n            element: <SettingsAbout />,\n          },\n        ],\n      },\n    ],\n  },\n  {\n    path: \"/login\",\n    element: <AuthLayout />,\n    children: [\n      {\n        path: \"/login\",\n        element: <Login />,\n      },\n    ],\n  },\n  {\n    path: \"*\",\n    element: (\n      <ErrorLayout>\n        <div className=\"flex h-screen w-full flex-col items-center justify-center\">\n          <div className=\"flex flex-wrap items-center justify-center gap-x-4 gap-y-2\">\n            <h1>404</h1>\n            <h2>This page could not be found.</h2>\n          </div>\n        </div>\n      </ErrorLayout>\n    ),\n  },\n]);\n"
  },
  {
    "path": "ui/src/stores/access/index.ts",
    "content": "﻿import { produce } from \"immer\";\nimport { create } from \"zustand\";\n\nimport { type AccessModel } from \"@/domain/access\";\nimport { list as listAccesses, remove as removeAccess, save as saveAccess } from \"@/repository/access\";\n\nimport { type AccessesState, type AccessesStore } from \"./types\";\n\nexport const useAccessesStore = create<AccessesStore>((set, get) => {\n  let fetcher: Promise<AccessModel[]> | null = null; // 防止多次重复请求\n\n  return {\n    accesses: [],\n    loading: false,\n    loadedAtOnce: false,\n\n    fetchAccesses: async (refresh = true) => {\n      if (!refresh) {\n        if (get().loadedAtOnce) {\n          return get().accesses;\n        }\n      }\n\n      fetcher ??= listAccesses().then((res) => res.items);\n\n      try {\n        set({ loading: true });\n        const accesses = await fetcher;\n        set({ accesses: accesses ?? [], loadedAtOnce: true });\n      } finally {\n        fetcher = null;\n        set({ loading: false });\n      }\n\n      return get().accesses;\n    },\n\n    createAccess: async (access) => {\n      const record = await saveAccess(access);\n      set(\n        produce((state: AccessesState) => {\n          state.accesses.unshift(record);\n        })\n      );\n\n      return record as AccessModel;\n    },\n\n    updateAccess: async (access) => {\n      const record = await saveAccess(access);\n      set(\n        produce((state: AccessesState) => {\n          const index = state.accesses.findIndex((e) => e.id === record.id);\n          if (index !== -1) {\n            state.accesses[index] = record;\n          }\n        })\n      );\n\n      return record as AccessModel;\n    },\n\n    deleteAccess: async (access) => {\n      await removeAccess(access);\n      if (Array.isArray(access)) {\n        set(\n          produce((state: AccessesState) => {\n            state.accesses = state.accesses.filter((e) => !access.some((item) => item.id === e.id));\n          })\n        );\n      } else {\n        set(\n          produce((state: AccessesState) => {\n            state.accesses = state.accesses.filter((e) => e.id !== access.id);\n          })\n        );\n      }\n\n      return access as AccessModel;\n    },\n  };\n});\n"
  },
  {
    "path": "ui/src/stores/access/types.ts",
    "content": "﻿import { type AccessModel } from \"@/domain/access\";\n\nexport interface AccessesState {\n  accesses: AccessModel[];\n  loading: boolean;\n  loadedAtOnce: boolean;\n}\n\nexport interface AccessesActions {\n  fetchAccesses: (refresh?: boolean) => Promise<AccessModel[]>;\n  createAccess: (access: MaybeModelRecord<AccessModel>) => Promise<AccessModel>;\n  updateAccess: (access: MaybeModelRecordWithId<AccessModel>) => Promise<AccessModel>;\n  deleteAccess: (access: MaybeModelRecordWithId<AccessModel> | MaybeModelRecordWithId<AccessModel>[]) => Promise<AccessModel>;\n}\n\nexport interface AccessesStore extends AccessesState, AccessesActions {}\n"
  },
  {
    "path": "ui/src/stores/settings/contact/index.ts",
    "content": "﻿import { produce } from \"immer\";\nimport { create } from \"zustand\";\n\nimport { type EmailsSettingsContent, SETTINGS_NAMES, type SettingsModel } from \"@/domain/settings\";\nimport { get as getSettings, save as saveSettings } from \"@/repository/settings\";\n\nimport { type ContactEmailsState, type ContactEmailsStore } from \"./types\";\n\nexport const useContactEmailsStore = create<ContactEmailsStore>((set, get) => {\n  let fetcher: Promise<SettingsModel<EmailsSettingsContent>> | null = null; // 防止多次重复请求\n  let model: SettingsModel<EmailsSettingsContent>; // 记录当前设置的其他字段，保存回数据库时用\n\n  return {\n    emails: [],\n    loading: false,\n    loadedAtOnce: false,\n\n    fetchEmails: async (refresh = true) => {\n      if (!refresh) {\n        if (get().loadedAtOnce) {\n          return get().emails;\n        }\n      }\n\n      fetcher ??= getSettings(SETTINGS_NAMES.EMAILS);\n\n      try {\n        set({ loading: true });\n        model = await fetcher;\n        set({ emails: model.content.emails?.filter((s) => !!s)?.sort() ?? [], loadedAtOnce: true });\n      } finally {\n        fetcher = null;\n        set({ loading: false });\n      }\n\n      return get().emails;\n    },\n\n    setEmails: async (emails) => {\n      model ??= await getSettings(SETTINGS_NAMES.EMAILS);\n      model = await saveSettings<EmailsSettingsContent>({\n        ...model,\n        content: {\n          ...model.content,\n          emails: emails,\n        },\n      });\n\n      set(\n        produce((state: ContactEmailsState) => {\n          state.emails = model.content.emails?.sort() ?? [];\n          state.loadedAtOnce = true;\n        })\n      );\n    },\n\n    addEmail: async (email) => {\n      const emails = produce(get().emails, (draft) => {\n        if (draft.includes(email)) return;\n        draft.push(email);\n        draft.sort();\n        return draft;\n      });\n      get().setEmails(emails);\n    },\n\n    removeEmail: async (email) => {\n      const emails = produce(get().emails, (draft) => {\n        draft = draft.filter((e) => e !== email);\n        draft.sort();\n        return draft;\n      });\n      get().setEmails(emails);\n    },\n  };\n});\n"
  },
  {
    "path": "ui/src/stores/settings/contact/types.ts",
    "content": "﻿export interface ContactEmailsState {\n  emails: string[];\n  loading: boolean;\n  loadedAtOnce: boolean;\n}\n\nexport interface ContactEmailsActions {\n  fetchEmails: (refresh?: boolean) => Promise<string[]>;\n  setEmails: (emails: string[]) => Promise<void>;\n  addEmail: (email: string) => Promise<void>;\n  removeEmail: (email: string) => Promise<void>;\n}\n\nexport interface ContactEmailsStore extends ContactEmailsState, ContactEmailsActions {}\n"
  },
  {
    "path": "ui/src/stores/settings/index.ts",
    "content": "﻿export { useContactEmailsStore } from \"./contact\";\nexport { usePersistenceSettingsStore } from \"./persistence\";\nexport { useSSLProviderSettingsStore } from \"./sslprovider\";\nexport { useNotifyTemplatesStore, useScriptTemplatesStore } from \"./template\";\n"
  },
  {
    "path": "ui/src/stores/settings/persistence/index.ts",
    "content": "﻿import { produce } from \"immer\";\nimport { create } from \"zustand\";\n\nimport { type PersistenceSettingsContent, SETTINGS_NAMES, type SettingsModel } from \"@/domain/settings\";\nimport { get as getSettings, save as saveSettings } from \"@/repository/settings\";\n\nimport { type PersistenceSettingsState, type PersistenceSettingsStore } from \"./types\";\n\nexport const usePersistenceSettingsStore = create<PersistenceSettingsStore>((set, get) => {\n  let fetcher: Promise<SettingsModel<PersistenceSettingsContent>> | null = null; // 防止多次重复请求\n  let model: SettingsModel<PersistenceSettingsContent>; // 记录当前设置的其他字段，保存回数据库时用\n\n  return {\n    settings: {} as PersistenceSettingsContent,\n    loading: false,\n    loadedAtOnce: false,\n\n    loadSettings: async (refresh = true) => {\n      if (!refresh) {\n        if (get().loadedAtOnce) {\n          return;\n        }\n      }\n\n      fetcher ??= getSettings(SETTINGS_NAMES.PERSISTENCE);\n\n      try {\n        set({ loading: true });\n        model = await fetcher;\n        set({ settings: model.content, loadedAtOnce: true });\n      } finally {\n        fetcher = null;\n        set({ loading: false });\n      }\n    },\n\n    saveSettings: async (settings) => {\n      model ??= await getSettings(SETTINGS_NAMES.PERSISTENCE);\n      model = await saveSettings<PersistenceSettingsContent>({\n        ...model,\n        content: settings,\n      });\n\n      set(\n        produce((state: PersistenceSettingsState) => {\n          state.settings = model.content;\n          state.loadedAtOnce = true;\n        })\n      );\n    },\n  };\n});\n"
  },
  {
    "path": "ui/src/stores/settings/persistence/types.ts",
    "content": "﻿import { type PersistenceSettingsContent } from \"@/domain/settings\";\n\nexport interface PersistenceSettingsState {\n  settings: PersistenceSettingsContent;\n  loading: boolean;\n  loadedAtOnce: boolean;\n}\n\nexport interface PersistenceSettingsActions {\n  loadSettings: (refresh?: boolean) => Promise<void>;\n  saveSettings: (settings: PersistenceSettingsContent) => Promise<void>;\n}\n\nexport interface PersistenceSettingsStore extends PersistenceSettingsState, PersistenceSettingsActions {}\n"
  },
  {
    "path": "ui/src/stores/settings/sslprovider/index.ts",
    "content": "﻿import { produce } from \"immer\";\nimport { create } from \"zustand\";\n\nimport { SETTINGS_NAMES, type SSLProviderSettingsContent, type SettingsModel } from \"@/domain/settings\";\nimport { get as getSettings, save as saveSettings } from \"@/repository/settings\";\n\nimport { type SSLProviderSettingsState, type SSLProviderSettingsStore } from \"./types\";\n\nexport const useSSLProviderSettingsStore = create<SSLProviderSettingsStore>((set, get) => {\n  let fetcher: Promise<SettingsModel<SSLProviderSettingsContent>> | null = null; // 防止多次重复请求\n  let model: SettingsModel<SSLProviderSettingsContent>; // 记录当前设置的其他字段，保存回数据库时用\n\n  return {\n    settings: {} as SSLProviderSettingsContent,\n    loading: false,\n    loadedAtOnce: false,\n\n    loadSettings: async (refresh = true) => {\n      if (!refresh) {\n        if (get().loadedAtOnce) {\n          return;\n        }\n      }\n\n      fetcher ??= getSettings(SETTINGS_NAMES.SSL_PROVIDER);\n\n      try {\n        set({ loading: true });\n        model = await fetcher;\n        set({ settings: model.content, loadedAtOnce: true });\n      } finally {\n        fetcher = null;\n        set({ loading: false });\n      }\n    },\n\n    saveSettings: async (settings) => {\n      model ??= await getSettings(SETTINGS_NAMES.SSL_PROVIDER);\n      model = await saveSettings<SSLProviderSettingsContent>({\n        ...model,\n        content: settings,\n      });\n\n      set(\n        produce((state: SSLProviderSettingsState) => {\n          state.settings = model.content;\n          state.loadedAtOnce = true;\n        })\n      );\n    },\n  };\n});\n"
  },
  {
    "path": "ui/src/stores/settings/sslprovider/types.ts",
    "content": "﻿import { type SSLProviderSettingsContent } from \"@/domain/settings\";\n\nexport interface SSLProviderSettingsState {\n  settings: SSLProviderSettingsContent;\n  loading: boolean;\n  loadedAtOnce: boolean;\n}\n\nexport interface SSLProviderSettingsActions {\n  loadSettings: (refresh?: boolean) => Promise<void>;\n  saveSettings: (settings: SSLProviderSettingsContent) => Promise<void>;\n}\n\nexport interface SSLProviderSettingsStore extends SSLProviderSettingsState, SSLProviderSettingsActions {}\n"
  },
  {
    "path": "ui/src/stores/settings/template/index.ts",
    "content": "﻿import { produce } from \"immer\";\nimport { create } from \"zustand\";\n\nimport { type NotifyTemplateContent, SETTINGS_NAMES, type ScriptTemplateContent, type SettingsModel } from \"@/domain/settings\";\nimport { get as getSettings, save as saveSettings } from \"@/repository/settings\";\n\nimport { type NotifyTemplatesState, type NotifyTemplatesStore, type ScriptTemplatesState, type ScriptTemplatesStore } from \"./types\";\n\nexport const useNotifyTemplatesStore = create<NotifyTemplatesStore>((set, get) => {\n  let fetcher: Promise<SettingsModel<NotifyTemplateContent>> | null = null; // 防止多次重复请求\n  let model: SettingsModel<NotifyTemplateContent>; // 记录当前设置的其他字段，保存回数据库时用\n\n  return {\n    templates: [],\n    loading: false,\n    loadedAtOnce: false,\n\n    fetchTemplates: async (refresh = true) => {\n      if (!refresh) {\n        if (get().loadedAtOnce) {\n          return;\n        }\n      }\n\n      fetcher ??= getSettings(SETTINGS_NAMES.NOTIFY_TEMPLATE);\n\n      try {\n        set({ loading: true });\n        model = await fetcher;\n        set({ templates: model.content.templates ?? [], loadedAtOnce: true });\n      } finally {\n        fetcher = null;\n        set({ loading: false });\n      }\n    },\n\n    setTemplates: async (templates) => {\n      model ??= await getSettings(SETTINGS_NAMES.NOTIFY_TEMPLATE);\n      model = await saveSettings<NotifyTemplateContent>({\n        ...model,\n        content: {\n          ...model.content,\n          templates: templates,\n        },\n      });\n\n      set(\n        produce((state: NotifyTemplatesState) => {\n          state.templates = model.content.templates ?? [];\n          state.loadedAtOnce = true;\n        })\n      );\n    },\n\n    addTemplate: async (template) => {\n      const templates = produce(get().templates, (draft) => {\n        const index = draft.findIndex((t) => t.name === template.name);\n        if (index !== -1) {\n          draft[index] = template;\n        } else {\n          draft.push(template);\n        }\n\n        return draft;\n      });\n      get().setTemplates(templates);\n    },\n\n    removeTemplateByIndex: async (index) => {\n      const templates = produce(get().templates, (draft) => {\n        draft = draft.filter((_, i) => i !== index);\n        return draft;\n      });\n      get().setTemplates(templates);\n    },\n\n    removeTemplateByName: async (name) => {\n      const templates = produce(get().templates, (draft) => {\n        draft = draft.filter((e) => e.name !== name);\n        return draft;\n      });\n      get().setTemplates(templates);\n    },\n  };\n});\n\nexport const useScriptTemplatesStore = create<ScriptTemplatesStore>((set, get) => {\n  let fetcher: Promise<SettingsModel<ScriptTemplateContent>> | null = null; // 防止多次重复请求\n  let model: SettingsModel<ScriptTemplateContent>; // 记录当前设置的其他字段，保存回数据库时用\n\n  return {\n    templates: [],\n    loading: false,\n    loadedAtOnce: false,\n\n    fetchTemplates: async (refresh = true) => {\n      if (!refresh) {\n        if (get().loadedAtOnce) {\n          return;\n        }\n      }\n\n      fetcher ??= getSettings(SETTINGS_NAMES.SCRIPT_TEMPLATE);\n\n      try {\n        set({ loading: true });\n        model = await fetcher;\n        set({ templates: model.content.templates ?? [], loadedAtOnce: true });\n      } finally {\n        fetcher = null;\n        set({ loading: false });\n      }\n    },\n\n    setTemplates: async (templates) => {\n      model ??= await getSettings(SETTINGS_NAMES.SCRIPT_TEMPLATE);\n      model = await saveSettings<ScriptTemplateContent>({\n        ...model,\n        content: {\n          ...model.content,\n          templates: templates,\n        },\n      });\n\n      set(\n        produce((state: ScriptTemplatesState) => {\n          state.templates = model.content.templates ?? [];\n          state.loadedAtOnce = true;\n        })\n      );\n    },\n\n    addTemplate: async (template) => {\n      const templates = produce(get().templates, (draft) => {\n        const index = draft.findIndex((t) => t.name === template.name);\n        if (index !== -1) {\n          draft[index] = template;\n        } else {\n          draft.push(template);\n        }\n\n        return draft;\n      });\n      get().setTemplates(templates);\n    },\n\n    removeTemplateByIndex: async (index) => {\n      const templates = produce(get().templates, (draft) => {\n        draft = draft.filter((_, i) => i !== index);\n        return draft;\n      });\n      get().setTemplates(templates);\n    },\n\n    removeTemplateByName: async (name) => {\n      const templates = produce(get().templates, (draft) => {\n        draft = draft.filter((e) => e.name !== name);\n        return draft;\n      });\n      get().setTemplates(templates);\n    },\n  };\n});\n"
  },
  {
    "path": "ui/src/stores/settings/template/types.ts",
    "content": "﻿type NotifyTemplate = {\n  name: string;\n  subject: string;\n  message: string;\n};\n\nexport interface NotifyTemplatesState {\n  templates: NotifyTemplate[];\n  loading: boolean;\n  loadedAtOnce: boolean;\n}\n\nexport interface NotifyTemplatesActions {\n  fetchTemplates: (refresh?: boolean) => Promise<void>;\n  setTemplates: (templates: NotifyTemplate[]) => Promise<void>;\n  addTemplate: (template: NotifyTemplate) => Promise<void>;\n  removeTemplateByIndex: (index: number) => Promise<void>;\n  removeTemplateByName: (name: string) => Promise<void>;\n}\n\nexport interface NotifyTemplatesStore extends NotifyTemplatesState, NotifyTemplatesActions {}\n\ntype ScriptTemplate = {\n  name: string;\n  command: string;\n};\n\nexport interface ScriptTemplatesState {\n  templates: ScriptTemplate[];\n  loading: boolean;\n  loadedAtOnce: boolean;\n}\n\nexport interface ScriptTemplatesActions {\n  fetchTemplates: (refresh?: boolean) => Promise<void>;\n  setTemplates: (templates: ScriptTemplate[]) => Promise<void>;\n  addTemplate: (template: ScriptTemplate) => Promise<void>;\n  removeTemplateByIndex: (index: number) => Promise<void>;\n  removeTemplateByName: (name: string) => Promise<void>;\n}\n\nexport interface ScriptTemplatesStore extends ScriptTemplatesState, ScriptTemplatesActions {}\n"
  },
  {
    "path": "ui/src/stores/workflow/index.ts",
    "content": "import { produce } from \"immer\";\nimport { isEqual } from \"radash\";\nimport { create } from \"zustand\";\n\nimport { WORKFLOW_NODE_TYPES, type WorkflowModel, type WorkflowNodeConfigForStart } from \"@/domain/workflow\";\nimport { get as getWorkflow, save as saveWorkflow, subscribe as subscribeWorkflow } from \"@/repository/workflow\";\n\nimport { type WorkflowStore } from \"./types\";\n\nexport const useWorkflowStore = create<WorkflowStore>((set, get) => {\n  const ensureInitialized = () => {\n    if (!get().initialized) throw \"Workflow not initialized yet\";\n  };\n\n  let unsubscriber: (() => void) | undefined;\n\n  return {\n    workflow: {} as WorkflowModel,\n    initialized: false,\n\n    init: async (id: string) => {\n      const data = await getWorkflow(id);\n      set({\n        workflow: data,\n        initialized: true,\n      });\n\n      unsubscriber ??= await subscribeWorkflow(id, (cb) => {\n        if (cb.record.id !== get().workflow.id) return;\n\n        set({\n          workflow: cb.record,\n        });\n      });\n    },\n\n    destroy: () => {\n      unsubscriber?.();\n      unsubscriber = void 0;\n\n      set({\n        workflow: {} as WorkflowModel,\n        initialized: false,\n      });\n    },\n\n    setName: async (name) => {\n      ensureInitialized();\n\n      const resp = await saveWorkflow({\n        id: get().workflow.id!,\n        name: name || \"\",\n      });\n\n      set((state) => {\n        return {\n          workflow: produce(state.workflow, (draft) => {\n            draft.name = resp.name;\n          }),\n        };\n      });\n    },\n\n    setDescription: async (description) => {\n      ensureInitialized();\n\n      const resp = await saveWorkflow({\n        id: get().workflow.id!,\n        description: description || \"\",\n      });\n\n      set((state) => {\n        return {\n          workflow: produce(state.workflow, (draft) => {\n            draft.description = resp.description;\n          }),\n        };\n      });\n    },\n\n    setEnabled: async (enabled) => {\n      ensureInitialized();\n\n      const resp = await saveWorkflow({\n        id: get().workflow.id!,\n        enabled: enabled,\n      });\n\n      set((state) => {\n        return {\n          workflow: produce(state.workflow, (draft) => {\n            draft.enabled = resp.enabled;\n          }),\n        };\n      });\n    },\n\n    orchestrate: async (graph) => {\n      ensureInitialized();\n\n      const resp = await saveWorkflow({\n        id: get().workflow.id!,\n        graphDraft: graph,\n        hasDraft: !isEqual(graph, get().workflow.graphContent),\n      });\n\n      set((state) => {\n        return {\n          workflow: produce(state.workflow, (draft) => {\n            draft.graphDraft = resp.graphDraft;\n            draft.hasDraft = resp.hasDraft;\n          }),\n        };\n      });\n    },\n\n    publish: async () => {\n      ensureInitialized();\n\n      const graph = get().workflow.graphDraft!;\n      if (graph?.nodes?.[0]?.type !== WORKFLOW_NODE_TYPES.START) throw \"Workflow nodes tree of draft in invalid\";\n      const startConfig = graph.nodes[0].data.config as WorkflowNodeConfigForStart;\n      const resp = await saveWorkflow({\n        id: get().workflow.id!,\n        trigger: startConfig.trigger,\n        triggerCron: startConfig.triggerCron,\n        graphContent: graph,\n        hasContent: true,\n        hasDraft: false,\n      });\n\n      set((state) => {\n        return {\n          workflow: produce(state.workflow, (draft) => {\n            draft.trigger = resp.trigger;\n            draft.triggerCron = resp.triggerCron;\n            draft.graphContent = resp.graphContent;\n            draft.hasContent = resp.hasContent;\n            draft.hasDraft = resp.hasDraft;\n          }),\n        };\n      });\n    },\n\n    rollback: async () => {\n      ensureInitialized();\n\n      const graph = get().workflow.graphContent!;\n      if (graph?.nodes?.[0]?.type !== WORKFLOW_NODE_TYPES.START) throw \"Workflow nodes tree of content in invalid\";\n      const startConfig = graph.nodes[0].data.config as WorkflowNodeConfigForStart;\n      const resp = await saveWorkflow({\n        id: get().workflow.id!,\n        trigger: startConfig.trigger,\n        triggerCron: startConfig.triggerCron,\n        hasContent: true,\n        graphDraft: graph,\n        hasDraft: false,\n      });\n\n      set((state) => {\n        return {\n          workflow: produce(state.workflow, (draft) => {\n            draft.trigger = resp.trigger;\n            draft.triggerCron = resp.triggerCron;\n            draft.hasContent = resp.hasContent;\n            draft.graphDraft = resp.graphDraft;\n            draft.hasDraft = resp.hasDraft;\n          }),\n        };\n      });\n    },\n  };\n});\n"
  },
  {
    "path": "ui/src/stores/workflow/types.ts",
    "content": "import { type WorkflowGraph, type WorkflowModel } from \"@/domain/workflow\";\n\nexport interface WorkflowState {\n  workflow: WorkflowModel;\n  initialized: boolean;\n}\n\nexport interface WorkflowActions {\n  init(id: string): void;\n  destroy(): void;\n\n  setName: (name: Required<WorkflowModel>[\"name\"]) => void;\n  setDescription: (description: Required<WorkflowModel>[\"description\"]) => void;\n  setEnabled(enabled: Required<WorkflowModel>[\"enabled\"]): void;\n\n  orchestrate(graph: WorkflowGraph): void;\n  publish(): void;\n  rollback(): void;\n}\n\nexport interface WorkflowStore extends WorkflowState, WorkflowActions {}\n"
  },
  {
    "path": "ui/src/utils/browser.ts",
    "content": "﻿export const isBrowserHappy = () => {\n  try {\n    if (typeof Promise.withResolvers !== \"function\") return false;\n    if (typeof Promise.try !== \"function\") return false;\n    if (typeof CSS.supports !== \"function\") return false;\n    if (!CSS.supports(\"color\", \"oklch(0 0 0)\")) return false;\n  } catch (_) {\n    return false;\n  }\n\n  return true;\n};\n"
  },
  {
    "path": "ui/src/utils/cron.ts",
    "content": "﻿import { CronExpressionParser } from \"cron-parser\";\n\nexport const validateCronExpression = (expr: string): boolean => {\n  try {\n    CronExpressionParser.parse(expr);\n\n    // pocketbase 后端仅支持标准 crontab 形式的表达式\n    // 这里转译了来自 pocketbase 的 golang 代码来验证\n    const segments = expr.trim().split(\" \");\n    if (segments.length !== 5) return false;\n    parseCronSegment(segments[0], 0, 59);\n    parseCronSegment(segments[1], 0, 23);\n    parseCronSegment(segments[2], 1, 31);\n    parseCronSegment(segments[3], 1, 12);\n    parseCronSegment(segments[4], 0, 6);\n\n    return true;\n  } catch {\n    return false;\n  }\n};\n\nexport const getNextCronExecutions = (expr: string, times = 1): Date[] => {\n  if (!validateCronExpression(expr)) return [];\n\n  const now = new Date();\n  const cron = CronExpressionParser.parse(expr, { currentDate: now });\n\n  return cron.take(times).map((date) => date.toDate());\n};\n\n// transpile from:\n//   https://github.com/pocketbase/pocketbase/blob/5d964c1b1d020f425299b32df03ecf44e0a0502e/tools/cron/schedule.go#L141-L218\nfunction parseCronSegment(segment: string, min: number, max: number): Set<number> {\n  const slots = new Set<number>();\n\n  const list = segment.split(\",\");\n  for (const p of list) {\n    const stepParts = p.split(\"/\");\n\n    let step: number;\n    switch (stepParts.length) {\n      case 1:\n        {\n          step = 1;\n        }\n        break;\n      case 2:\n        {\n          const parsedStep = parseInt(stepParts[1], 10);\n          if (isNaN(parsedStep) || parsedStep < 1 || parsedStep > max) {\n            throw new Error(`Invalid segment step boundary - the step must be between 1 and the ${max}`);\n          }\n          step = parsedStep;\n        }\n        break;\n\n      default:\n        throw new Error(\"Invalid segment step format - must be in the format */n or 1-30/n\");\n    }\n\n    let rangeMin: number, rangeMax: number;\n    if (stepParts[0] === \"*\") {\n      rangeMin = min;\n      rangeMax = max;\n    } else {\n      const rangeParts = stepParts[0].split(\"-\");\n      switch (rangeParts.length) {\n        case 1:\n          {\n            if (step !== 1) {\n              throw new Error(\"Invalid segment step - step > 1 could be used only with the wildcard or range format\");\n            }\n            const parsed = parseInt(rangeParts[0], 10);\n            if (isNaN(parsed) || parsed < min || parsed > max) {\n              throw new Error(\"Invalid segment value - must be between the min and max of the segment\");\n            }\n            rangeMin = parsed;\n            rangeMax = rangeMin;\n          }\n          break;\n\n        case 2:\n          {\n            const parsedMin = parseInt(rangeParts[0], 10);\n            if (isNaN(parsedMin) || parsedMin < min || parsedMin > max) {\n              throw new Error(`Invalid segment range minimum - must be between ${min} and ${max}`);\n            }\n            rangeMin = parsedMin;\n\n            const parsedMax = parseInt(rangeParts[1], 10);\n            if (isNaN(parsedMax) || parsedMax < rangeMin || parsedMax > max) {\n              throw new Error(`Invalid segment range maximum - must be between ${rangeMin} and ${max}`);\n            }\n            rangeMax = parsedMax;\n          }\n          break;\n\n        default:\n          throw new Error(\"Invalid segment range format - the range must have 1 or 2 parts\");\n      }\n    }\n\n    for (let i = rangeMin; i <= rangeMax; i += step) {\n      slots.add(i);\n    }\n  }\n\n  return slots;\n}\n"
  },
  {
    "path": "ui/src/utils/css.ts",
    "content": "﻿import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport const mergeCls = (...inputs: ClassValue[]) => {\n  return twMerge(clsx(inputs));\n};\n"
  },
  {
    "path": "ui/src/utils/error.ts",
    "content": "import { ClientResponseError } from \"pocketbase\";\n\nexport const unwrapErrMsg = (error: unknown): string => {\n  if (error instanceof ClientResponseError) {\n    return Object.keys(error.response ?? {}).length ? unwrapErrMsg(error.response) : error.message;\n  } else if (error instanceof Error) {\n    return error.message;\n  } else if (typeof error === \"object\" && error != null) {\n    if (\"message\" in error) {\n      return unwrapErrMsg(error.message);\n    } else if (\"msg\" in error) {\n      return unwrapErrMsg(error.msg);\n    }\n  } else if (typeof error === \"string\") {\n    return error || \"Unknown error\";\n  }\n\n  return \"Unknown error\";\n};\n"
  },
  {
    "path": "ui/src/utils/file.ts",
    "content": "export const readFileAsText = (file: File): Promise<string> => {\n  const { promise, resolve, reject } = Promise.withResolvers<string>();\n\n  const reader = new FileReader();\n  reader.onload = () => {\n    if (reader.result != null) {\n      resolve(reader.result.toString());\n    } else {\n      reject(new Error(\"Read file failed: result is null\"));\n    }\n  };\n  reader.onerror = () => reject(reader.error);\n  reader.readAsText(file, \"utf-8\");\n\n  return promise;\n};\n"
  },
  {
    "path": "ui/src/utils/search.ts",
    "content": "﻿export const matchSearchString = (keyword: string, candidate: string) => {\n  keyword = String(keyword ?? \"\").toLowerCase();\n  candidate = String(candidate ?? \"\").toLowerCase();\n\n  if (keyword.length === 0) {\n    return false;\n  }\n\n  if (candidate.includes(keyword)) {\n    return true;\n  }\n\n  if (keyword.includes(\" \")) {\n    keyword = keyword.replaceAll(\" \", \"\");\n    candidate = candidate.replaceAll(\" \", \"\");\n    if (matchSearchString(keyword, candidate)) {\n      return true;\n    }\n  }\n\n  return false;\n};\n\nexport const matchSearchOption = (keyword: string, candidate: string | { label?: unknown } | { value?: unknown }) => {\n  if (typeof candidate === \"string\") {\n    return matchSearchString(keyword, candidate);\n  }\n\n  if (\"label\" in candidate && candidate.label != null) {\n    if (matchSearchString(keyword, candidate.label as string)) {\n      return true;\n    }\n  }\n\n  if (\"value\" in candidate && candidate.value != null) {\n    if (matchSearchString(keyword, candidate.value as string)) {\n      return true;\n    }\n  }\n\n  return false;\n};\n"
  },
  {
    "path": "ui/src/utils/validator.ts",
    "content": "﻿import { z } from \"zod\";\n\nimport { validateCronExpression } from \"./cron\";\n\nexport const isCron = (value: string) => {\n  return validateCronExpression(value);\n};\n\nexport const isDomain = (value: string, { allowWildcard = false }: { allowWildcard?: boolean } = {}) => {\n  const re = allowWildcard\n    ? /^(?:\\*\\.)?(?!-)[A-Za-z0-9-]{1,}(?<!-)(\\.[A-Za-z0-9-]{1,}(?<!-)){0,}(?<![-0-9])$/\n    : /^(?!-)[A-Za-z0-9-]{1,}(?<!-)(\\.[A-Za-z0-9-]{1,}(?<!-)){0,}(?<![-0-9])$/;\n  return re.test(value);\n};\n\nexport const isEmail = (value: string) => {\n  return z.email().safeParse(value).success;\n};\n\nexport const isHostname = (value: string) => {\n  return isDomain(value, { allowWildcard: false }) || isIPv4(value) || isIPv6(value);\n};\n\nexport const isIPv4 = (value: string) => {\n  return z.ipv4().safeParse(value).success;\n};\n\nexport const isIPv6 = (value: string) => {\n  return z.ipv6().safeParse(value).success;\n};\n\nexport const isJsonObject = (value: string) => {\n  try {\n    const obj = JSON.parse(value);\n    return typeof obj === \"object\" && !Array.isArray(obj);\n  } catch {\n    return false;\n  }\n};\n\nexport const isPortNumber = (value: string | number) => {\n  return z.coerce.number().int().min(1).max(65535).safeParse(value).success;\n};\n\nexport const isUrlWithHttp = (value: string) => {\n  return z.url().startsWith(\"http://\").safeParse(value).success;\n};\n\nexport const isUrlWithHttps = (value: string) => {\n  return z.url().startsWith(\"https://\").safeParse(value).success;\n};\n\nexport const isUrlWithHttpOrHttps = (value: string) => {\n  return isUrlWithHttp(value) || isUrlWithHttps(value);\n};\n"
  },
  {
    "path": "ui/src/utils/x509.ts",
    "content": "﻿import { ECPrivateKey } from \"@peculiar/asn1-ecc\";\nimport { PrivateKeyInfo } from \"@peculiar/asn1-pkcs8\";\nimport { RSAPrivateKey } from \"@peculiar/asn1-rsa\";\nimport { AsnParser } from \"@peculiar/asn1-schema\";\nimport { PemConverter, SubjectAlternativeNameExtension, X509Certificate } from \"@peculiar/x509\";\n\nexport const parseCertificate = (certPEM: string): X509Certificate => {\n  if (!X509Certificate.isAsnEncoded(certPEM)) {\n    throw new Error(\"Could not parse X.509 certificate. Maybe it is not in PEM format?\");\n  }\n\n  try {\n    const cert = new X509Certificate(certPEM);\n    if (cert == null) {\n      throw new Error(\"Parse PEM certificate failed: result is null\");\n    }\n\n    return cert;\n  } catch (err) {\n    throw new Error(\"Could not parse X.509 certificate\", { cause: err });\n  }\n};\n\nexport const getCertificateSubjectAltNames = (certificate: string | X509Certificate): string[] => {\n  try {\n    const certX509 = certificate instanceof X509Certificate ? certificate : parseCertificate(certificate);\n    if (certX509 == null) return [];\n\n    const sanExt = certX509.getExtension(SubjectAlternativeNameExtension);\n    return sanExt?.names?.items?.map((san) => san.value) || [];\n  } catch {\n    return [];\n  }\n};\n\nexport const parsePKCS1PrivateKey = (keyPEM: string): RSAPrivateKey => {\n  try {\n    const PEM_BLOCK_TYPE = \"RSA PRIVATE KEY\";\n    const pemBlock = PemConverter.decodeWithHeaders(keyPEM)[0];\n    if (!pemBlock || pemBlock.type !== PEM_BLOCK_TYPE) {\n      throw new Error(`PEM block is not of type '${PEM_BLOCK_TYPE}'`);\n    }\n\n    const key = AsnParser.parse(pemBlock.rawData, RSAPrivateKey);\n    if (key == null) {\n      throw new Error(\"Read private key failed: result is null\");\n    }\n\n    return key;\n  } catch (err) {\n    throw new Error(\"Could not parse PKCS#1 RSA private key\", { cause: err });\n  }\n};\n\nexport const parsePKCS8PrivateKey = (keyPEM: string): PrivateKeyInfo => {\n  try {\n    const PEM_BLOCK_TYPE = \"PRIVATE KEY\";\n    const pemBlock = PemConverter.decodeWithHeaders(keyPEM)[0];\n    if (!pemBlock || pemBlock.type !== PEM_BLOCK_TYPE) {\n      throw new Error(`PEM block is not of type '${PEM_BLOCK_TYPE}'`);\n    }\n\n    const key = AsnParser.parse(pemBlock.rawData, PrivateKeyInfo);\n    if (key == null) {\n      throw new Error(\"Read private key failed: result is null\");\n    }\n\n    return key;\n  } catch (err) {\n    throw new Error(\"Could not parse PKCS#8 private key\", { cause: err });\n  }\n};\n\nexport const parseECPrivateKey = (keyPEM: string): ECPrivateKey => {\n  try {\n    const PEM_BLOCK_TYPE = \"EC PRIVATE KEY\";\n    const pemBlock = PemConverter.decodeWithHeaders(keyPEM)[0];\n    if (!pemBlock || pemBlock.type !== PEM_BLOCK_TYPE) {\n      throw new Error(`PEM block is not of type '${PEM_BLOCK_TYPE}'`);\n    }\n\n    const key = AsnParser.parse(pemBlock.rawData, ECPrivateKey);\n    if (key == null) {\n      throw new Error(\"Read private key failed: result is null\");\n    }\n\n    return key;\n  } catch (err) {\n    throw new Error(\"Could not parse EC private key\", { cause: err });\n  }\n};\n\nexport const parsePrivateKey = (keyPEM: string): RSAPrivateKey | ECPrivateKey | PrivateKeyInfo => {\n  try {\n    return parsePKCS1PrivateKey(keyPEM);\n  } catch {\n    try {\n      return parseECPrivateKey(keyPEM);\n    } catch {\n      return parsePKCS8PrivateKey(keyPEM);\n    }\n  }\n};\n\nexport const getPrivateKeyAlgorithm = (keyPEM: string): { algorithm?: \"RSA\" | \"EC\"; keySize?: number } => {\n  try {\n    const key = parsePrivateKey(keyPEM);\n\n    if (key instanceof RSAPrivateKey) {\n      return { algorithm: \"RSA\", keySize: (key.modulus.byteLength - 1) * 8 };\n    }\n\n    if (key instanceof ECPrivateKey) {\n      return { algorithm: \"EC\", keySize: key.privateKey.byteLength * 8 };\n    }\n\n    if (key instanceof PrivateKeyInfo) {\n      const OLD_PUBKEY_RSA = \"1.2.840.113549.1.1.1\";\n      const OLD_PUBKEY_ECDSA = \"1.2.840.10045.2.1\";\n      if (key.privateKeyAlgorithm.algorithm === OLD_PUBKEY_RSA) {\n        const rsaKey = AsnParser.parse(key.privateKey, RSAPrivateKey);\n        return { algorithm: \"RSA\", keySize: (rsaKey.modulus.byteLength - 1) * 8 };\n      }\n      if (key.privateKeyAlgorithm.algorithm === OLD_PUBKEY_ECDSA) {\n        const ecKey = AsnParser.parse(key.privateKey, ECPrivateKey);\n        return { algorithm: \"EC\", keySize: ecKey.privateKey.byteLength * 8 };\n      }\n    }\n\n    return {};\n  } catch {\n    return {};\n  }\n};\n\nexport const validatePEMCertificate = (certPEM: string): boolean => {\n  try {\n    const cert = parseCertificate(certPEM);\n    return !!cert.getExtension(SubjectAlternativeNameExtension);\n  } catch {\n    return false;\n  }\n};\n\nexport const validatePEMPrivateKey = (keyPEM: string): boolean => {\n  try {\n    parsePrivateKey(keyPEM);\n    return true;\n  } catch {\n    return false;\n  }\n};\n"
  },
  {
    "path": "ui/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\n      \"DOM\",\n      \"DOM.Iterable\",\n      \"ESNext\",\n    ],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\n        \"./src/*\"\n      ]\n    },\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"include\": [\n    \"src\",\n    \"types\"\n  ]\n}\n"
  },
  {
    "path": "ui/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.app.json\"\n    },\n    {\n      \"path\": \"./tsconfig.node.json\"\n    }\n  ],\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\n        \"src/*\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "ui/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"noEmit\": true\n  },\n  \"include\": [\n    \"vite.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "ui/types/global.d.ts",
    "content": "﻿import { type BaseModel as PbBaseModel } from \"pocketbase\";\n\ndeclare global {\n  declare type ISO8601String = string;\n\n  declare interface BaseModel extends PbBaseModel {\n    created: ISO8601String;\n    updated: ISO8601String;\n    deleted?: ISO8601String;\n  }\n\n  declare type MaybeModelRecord<T extends BaseModel = BaseModel> = T | Omit<T, \"id\" | \"created\" | \"updated\" | \"deleted\">;\n\n  declare type MaybeModelRecordWithId<T extends BaseModel = BaseModel> = T | Pick<T, \"id\">;\n\n  declare interface BaseResponse<T = any> {\n    code: number;\n    msg: string;\n    data: T;\n  }\n}\n\nexport {};\n"
  },
  {
    "path": "ui/types/global.utility.d.ts",
    "content": "﻿declare global {\n  type Nullish<T> = {\n    [P in keyof T]?: T[P] | null | undefined;\n  };\n\n  type ArrayElement<T> = T extends (infer U)[] ? U : never;\n}\n\nexport {};\n"
  },
  {
    "path": "ui/types/shims-antd.d.ts",
    "content": "declare module \"antd/locale/zh_CN\" {\n  import zhCN from \"antd/locale/zh_CN\";\n  export default zhCN;\n}\n"
  },
  {
    "path": "ui/types/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\ndeclare const __APP_VERSION__: string;\n"
  },
  {
    "path": "ui/vite.config.ts",
    "content": "import path from \"node:path\";\n\nimport tailwindcssPlugin from \"@tailwindcss/vite\";\nimport legacyPlugin from \"@vitejs/plugin-legacy\";\nimport reactPlugin from \"@vitejs/plugin-react\";\nimport fs from \"fs-extra\";\nimport { type Plugin, defineConfig } from \"vite\";\n\nconst preserveFilesPlugin = (filesToPreserve: string[]): Plugin => {\n  return {\n    name: \"preserve-files\",\n    apply: \"build\",\n    buildStart() {\n      // 在构建开始时将要保留的文件或目录移动到临时位置\n      filesToPreserve.forEach((file) => {\n        const srcPath = path.resolve(__dirname, file);\n        const tempPath = path.resolve(__dirname, `node_modules/.tmp/build/${file}`);\n        if (fs.existsSync(srcPath)) {\n          fs.moveSync(srcPath, tempPath, { overwrite: true });\n        }\n      });\n    },\n    closeBundle() {\n      // 在构建完成后将临时位置的文件或目录移回原来的位置\n      filesToPreserve.forEach((file) => {\n        const srcPath = path.resolve(__dirname, file);\n        const tempPath = path.resolve(__dirname, `node_modules/.tmp/build/${file}`);\n        if (fs.existsSync(tempPath)) {\n          fs.moveSync(tempPath, srcPath, { overwrite: true });\n        }\n      });\n    },\n  };\n};\n\nexport default defineConfig(() => {\n  let appVersion = undefined;\n  try {\n    const content = fs.readFileSync(path.resolve(__dirname, \"../internal/app/app.go\"), \"utf-8\");\n    const matches = content.match(/AppVersion\\s+=\\s+\"(.+?)\"/);\n    if (matches) {\n      appVersion = matches[1];\n      console.info(\"[certimate] AppVersion is \" + appVersion);\n    } else {\n      throw new Error(\"AppVersion not found in '/internal/app/app.go'\");\n    }\n  } catch (err) {\n    throw new Error(\"Could not read app version: \" + (err as Error).message);\n  }\n\n  return {\n    define: {\n      __APP_VERSION__: JSON.stringify(appVersion),\n    },\n    build: {\n      rollupOptions: {\n        output: {\n          manualChunks(id) {\n            if (id.includes(\"/src/i18n/\")) {\n              return \"locales\";\n            }\n          },\n        },\n      },\n    },\n    plugins: [\n      reactPlugin({}),\n      legacyPlugin({\n        targets: [\"defaults\", \"not IE 11\"],\n        modernTargets: \"chrome>=111, firefox>=113, safari>=15.4\",\n        polyfills: true,\n        modernPolyfills: true,\n        renderLegacyChunks: false,\n        renderModernChunks: true,\n      }),\n      tailwindcssPlugin(),\n      preserveFilesPlugin([\"dist/.gitkeep\"]),\n    ],\n    resolve: {\n      alias: {\n        \"@\": path.resolve(__dirname, \"./src\"),\n      },\n    },\n    server: {\n      proxy: {\n        \"/api\": \"http://127.0.0.1:8090\",\n      },\n    },\n  };\n});\n"
  }
]