[
  {
    "path": ".dockerignore",
    "content": "fly.toml"
  },
  {
    "path": ".github/slack-notification.json",
    "content": "{\n  \"url\": \"${SLACK_WEBHOOK_URL}\",\n  \"payload\": {\n    \"blocks\": [\n      {\n        \"type\": \"header\",\n        \"text\": {\n          \"type\": \"plain_text\",\n          \"text\": \"Workflow breakpoint started\",\n          \"emoji\": true\n        }\n      },\n      {\n        \"type\": \"section\",\n        \"text\": {\n          \"type\": \"mrkdwn\",\n          \"text\": \"*Repository:* <https://github.com/${GITHUB_REPOSITORY}/tree/${GITHUB_REF_NAME}|${GITHUB_REPOSITORY}> (${GITHUB_REF_NAME})\"\n        }\n      },\n      {\n        \"type\": \"section\",\n        \"text\": {\n          \"type\": \"mrkdwn\",\n          \"text\": \"*Workflow:* ${GITHUB_WORKFLOW} (<https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}|Run #${GITHUB_RUN_NUMBER}>)\"\n        }\n      },\n      {\n        \"type\": \"section\",\n        \"text\": {\n          \"type\": \"mrkdwn\",\n          \"text\": \"*SSH:* `ssh -p ${BREAKPOINT_PORT} runner@${BREAKPOINT_HOST}`\"\n        }\n      },\n      {\n        \"type\": \"section\",\n        \"text\": {\n          \"type\": \"mrkdwn\",\n          \"text\": \"*Expires:* in ${BREAKPOINT_TIME_LEFT} (${BREAKPOINT_EXPIRATION})\"\n        }\n      },\n      {\n        \"type\": \"context\",\n        \"elements\": [\n          {\n            \"type\": \"plain_text\",\n            \"text\": \"Actor: ${GITHUB_ACTOR}\",\n            \"emoji\": true\n          }\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n\npermissions:\n  contents: read # Checkout the code\n  packages: write # Push to GitHub registry\n\nenv:\n  IMAGE_NAME: rendezvous\n  IMAGE_REPO: ghcr.io/${{ github.repository_owner }}\n  VERSION: ${{ github.sha }}\n\njobs:\n  docker-build:\n    name: Build with Docker\n    runs-on: nscloud\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v2\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Setup Buildx for Docker build\n        uses: docker/setup-buildx-action@v2\n\n      - name: Docker build the Rendezvous server\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          push: true\n          tags: ${{ env.IMAGE_REPO }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}\n\n      - name: Breakpoint on failure\n        if: failure()\n        uses: namespacelabs/breakpoint-action@v0\n        env:\n          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}\n        with:\n          duration: 30m\n          authorized-users: hugosantos,n-g,htr\n          slack-announce-channel: \"#ci\"\n"
  },
  {
    "path": ".github/workflows/checks.yml",
    "content": "name: Commit Checks\non:\n  pull_request:\n    branches:\n      - \"*\"\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n\npermissions:\n  contents: read # Checkout the code\n\njobs:\n  checks:\n    name: Code Checks\n    runs-on: nscloud-ubuntu-22.04-amd64-2x8\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n      - name: Install Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: 'stable'\n\n      - name: Check Go formatting\n        run: go fmt ./... && git diff --exit-code\n\n      - name: Check Go mod is tidy\n        run: go mod tidy && git diff --exit-code\n\n      - name: Check that Go builds\n        run: |\n          go build -o . ./cmd/...\n\n      - name: Breakpoint on failure\n        if: failure()\n        uses: namespacelabs/breakpoint-action@v0\n        env:\n          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}\n        with:\n          duration: 30m\n          authorized-users: hugosantos,n-g,htr\n          slack-announce-channel: \"#ci\"\n"
  },
  {
    "path": ".github/workflows/goreleaser.yml",
    "content": "name: Release Binaries\n\non:\n  push:\n    tags: [\"v*\"]\n\njobs:\n  goreleaser:\n    runs-on: nscloud-ubuntu-24.04-amd64-4x8\n    permissions:\n      contents: write\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: \"1.20\"\n\n      - uses: goreleaser/goreleaser-action@v6\n        with:\n          args: release --clean\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release Rendezvous Docker image\n\non:\n  release:\n    types: [released]\n\npermissions:\n  contents: read # Checkout the code\n  packages: write # Push to GitHub registry\n\nenv:\n  IMAGE_NAME: rendezvous\n  IMAGE_REPO: ghcr.io/${{ github.repository_owner }}\n  VERSION: ${{ github.event.release.tag_name }}\n\njobs:\n  docker-release:\n    name: Release Docker image ${{ github.event.release.tag_name }}\n    runs-on: nscloud-ubuntu-22.04-amd64-2x8\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v2\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Setup Buildx for Docker build\n        uses: namespacelabs/nscloud-setup-buildx-action@v0\n\n      - name: Docker build the Rendezvous server\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          push: true\n          tags: ${{ env.IMAGE_REPO }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}\n          platforms: linux/amd64,linux/arm64\n\n      - name: Breakpoint on failure\n        if: failure()\n        uses: namespacelabs/breakpoint-action@v0\n        env:\n          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}\n        with:\n          duration: 30m\n          authorized-users: hugosantos,n-g,htr\n          slack-announce-channel: \"#ci\"\n"
  },
  {
    "path": ".gitignore",
    "content": "# If you prefer the allow list template instead of the deny list, see community template:\n# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore\n#\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\n# vendor/\n\n# Go workspace file\ngo.work\n\ndist/\n"
  },
  {
    "path": ".goreleaser.yaml",
    "content": "before:\n  hooks:\n    # You may remove this if you don't use go modules.\n    - go mod tidy\nbuilds:\n  - id: breakpoint\n    main: ./cmd/breakpoint\n    binary: breakpoint\n    env:\n      - CGO_ENABLED=0\n    goos:\n      - linux\n      - darwin\n      - windows\n    goarch:\n      - amd64\n      - arm64\n\narchives:\n  - id: breakpoint\n    builds:\n      - breakpoint\n    name_template: \"breakpoint_{{ .Os }}_{{ .Arch }}\"\n\nrelease:\n  github:\n    owner: namespacelabs\n    name: breakpoint\n\nchecksum:\n  name_template: \"checksums.txt\"\nsnapshot:\n  name_template: \"{{ incpatch .Version }}-next\"\n\nchangelog:\n  sort: asc\n  filters:\n    exclude:\n      - \"^docs:\"\n      - \"^test:\"\n      - \"^nochangelog\"\n      - \"^Merge pull request\"\n      - \"^Merge branch\"\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:1.20-alpine AS builder\n\nWORKDIR /app\n\nCOPY go.mod ./\nCOPY go.sum ./\nRUN go mod download\n\nCOPY . .\n\nRUN go build ./cmd/rendezvous\n\nFROM cgr.dev/chainguard/static\n\nCOPY --from=builder /app/rendezvous /rendezvous\n\nCMD [ \"/rendezvous\" ]"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"https://raw.githubusercontent.com/namespacelabs/breakpoint/main/docs/imgs/breakpoint-banner.png\" alt=\"Breakpoint. Debug with SSH. Resume.\" width=\"400\" height=\"200\">\n\n[![Discord](https://img.shields.io/badge/Join-Namespace-blue?color=blue&label=Discord&logo=discord&logoColor=3eb0ff&style=flat-square)](https://discord.gg/DqMzDFR6Hc)\n[![Twitter Follow](https://img.shields.io/badge/Follow-Namespace_Labs-blue?logo=twitter&style=flat-square)](https://twitter.com/intent/follow?screen_name=namespacelabs)\n[![GitHub Actions](https://img.shields.io/badge/GitHub-Action-blue?logo=githubactions&style=flat-square)](https://github.com/namespacelabs/breakpoint-action)\n![GitHub](https://img.shields.io/github/license/namespacelabs/breakpoint?color=blue&label=License&style=flat-square)\n![Build](https://img.shields.io/github/actions/workflow/status/namespacelabs/breakpoint/build.yml?label=Build&style=flat-square)\n![Checks](https://img.shields.io/github/actions/workflow/status/namespacelabs/breakpoint/checks.yml?label=Checks&style=flat-square)\n\n# Breakpoint\n\nAdd breakpoints to CI (e.g. GitHub Action workflows): pause workflows, access the workflow with SSH, debug and resume executions.\n\n## What is Breakpoint\n\nHave you ever wished you could have debugged an issue in CI (e.g. GitHub Actions), by SSHing to where your build or tests are running?\n\nBreakpoint helps you create breakpoints in CI: stop the execution of the workflow, and jump in to live debug as needed with SSH (without compromising end-to-end encryption).\n\nYou can make changes, re-run commands, and resume the workflow as needed. Need more time? Just run `breakpoint extend` to extend your breakpoint duration.\n\nAnd it's 100% open-source (both client and server).\n\n> ℹ️ Workflows that have active breakpoints are still \"running\" and continue to count towards your total CI usage.\n\n## Using Breakpoint\n\nBreakpoint loves GitHub Actions. You can use the [Breakpoint Action](https://github.com/namespacelabs/breakpoint-action) to add a breakpoint to a GitHub workflow; but most importantly, you can add breakpoints that only trigger when there's a failure in the workflow.\n\nThe example below triggers the Breakpoint only if the previous step (i.e. `go test`) failed. When that happens, Breakpoint pauses the workflow for 30 minutes and allows SSH from GitHub users \"jack123\" and \"alice321\".\n\n```yaml\njobs:\n  go-tests:\n    runs-on: ubuntu-latest\n\n    permissions:\n      id-token: write\n      contents: read\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Run Go tests\n        runs: |\n          go test ./...\n\n      - name: Breakpoint if tests failed\n        if: failure()\n        uses: namespacelabs/breakpoint-action@v0\n        with:\n          duration: 30m\n          authorized-users: jack123, alice321\n```\n\nWhen Breakpoint activates, it will output on a regular basis how much time left\nthere is in the breakpoint, and which address to SSH to get to the workflow.\n\n```bash\n┌───────────────────────────────────────────────────────────────────────────┐\n│                                                                           │\n│ Breakpoint running until 2023-05-24T16:06:48+02:00 (29 minutes from now). │\n│                                                                           │\n│ Connect with: ssh -p 40812 runner@rendezvous.namespace.so                 │\n│                                                                           │\n└───────────────────────────────────────────────────────────────────────────┘\n```\n\nYou can now SSH the runner, re-run builds or tests, and even do changes.\n\nIf you need more time, run `breakpoint extend` to extend the breakpoint duration\nby 30 more minutes (or extend by more with the `--for` flag).\n\nWhen you are done, you can end the breakpoint session with `breakpoint resume`.\n\n> [!TIP]\n> You can also run Breakpoint in the background, by adding `mode: background` to the actions inputs. \\\n> That way, you can connect to it at any time during your workflow\n> \n> <details>\n>  <summary>Example</summary>\n>  <p>\n>\n> ```yaml\n>       - name: Breakpoint in the background\n>        uses: namespacelabs/breakpoint-action@v0\n>        with:\n>          mode: background\n>          authorized-users: jack123, alice321\n> ```\n> </p></details>\n>\n> [More info](https://github.com/namespacelabs/breakpoint-action?tab=readme-ov-file#run-in-the-background)\n\nBy default, the Breakpoint Action uses a shared `rendezvous` server provided by\nNamespace Labs for free. Even though a shared server is used, your SSH traffic is always _encrypted end-to-end_ (see Architecture).\n\nCheck out the [Breakpoint Action](https://github.com/namespacelabs/breakpoint-action) for more details on\nwhat arguments you can set.\n\n### Using the Breakpoint CLI to create a breakpoint\n\nTo activate a breakpoint, you can run:\n\n```bash\n$ breakpoint wait --config config.json\n```\n\nThe config file can look like as follows:\n\n```json\n{\n  \"endpoint\": \"rendezvous.namespace.so:5000\",\n  \"shell\": [\"/bin/bash\"],\n  \"allowed_ssh_users\": [\"runner\"],\n  \"authorized_keys\": [],\n  \"authorized_github_users\": [\"<your-github-username>\"],\n  \"duration\": \"30m\"\n}\n```\n\nThe `wait` command will block the caller and print an SSH endpoint that you can connect to:\n\n```bash\n┌───────────────────────────────────────────────────────────────────────────┐\n│                                                                           │\n│ Breakpoint running until 2023-05-24T16:06:48+02:00 (29 minutes from now). │\n│                                                                           │\n│ Connect with: ssh -p 40812 runner@rendezvous.namespace.so                 │\n│                                                                           │\n└───────────────────────────────────────────────────────────────────────────┘\n```\n\nOnce you are logged into the SSH session, you can use breakpoint CLI to extend the breakpoint duration, or resume the workflow (i.e. exit the `wait`):\n\n- `breakpoint extend --for 60m`: extend the wait period for 30m more minutes\n- `breakpoint resume`: stops Breakpoint process and release the control flow to the caller of the `wait` command\n\n## Architecture\n\nBreakpoint consists of two main components: `rendezvous` (where public connections are terminated) and `breakpoint`.\n\nWhen a breakpoint is created, the CLI blocks until an expiration time has passed.\n\nMeanwhile, it establishes a QUIC connection to `rendezvous`, which allocates a\npublic endpoint (with a random port) that will be reverse proxied back to the\nrunning `breakpoint`; each connection then serves a SSH session (from a ssh\nservice embedded in `breakpoint`). SSH sessions do not start new user sessions,\nand always run commands using the same uid as the parent `breakpoint wait` as\nwell.\n\nThe first QUIC stream `breakpoint -> rendezvous` is used for gRPC; `rendezvous`\nexpects a `Register` stream in order to allocate an endpoint, and will serve\nthat endpoint while the corresponding gRPC stream is active.\n\nBecause the SSH session is established end-to-end, `rendezvous` is not capable of performing a man-in-the-middle attack.\n\n![architecture](docs/imgs/Breakpoint%20high-level%20view.png)\n\nThe CLI implements pausing by blocking the caller process. The command\n`breakpoint wait` blocks until either the user runs `breakpoint resume` or the\nwait-timer expires. The communication between the `wait` process and the CLI is\nimplemented with gRPC.\n\nOn receive a connection, `rendezvous` establishes a new QUIC stream over the\nsame connection that was registered previously, in the direction `rendezvous -> breakpoint` and performs dumb TCP proxying over it, without the need of additional framing.\n\nThe lack of additional framing in addition to QUIC's streams having independent\ncontrol flow (i.e. no shared head of the line blocking), make QUIC a perfect\nsolution for this type of reverse proxying (in fact, cloudflare uses similar\ntechniques in Cloudflare Tunnel).\n\n## Authentication\n\nThe SSH service in `breakpoint` only accepts sessions from pre-referenced keys or public SSH keys configured by GitHub users. These are specified in the configuration file when the breakpoint is created (or as arguments to the GitHub action).\n\nYou can specify GitHub usernames in the `github_usernames` config field. Breakpoint automatically fetches the SSH public keys from GitHub for these users. You can also specify the SSH keys directly via the `authorized_keys` field.\n\nThe SSH service always spawns processes with the same uid as `breakpoint wait`, and by default accepts any requested username. This can be limited by setting the `allowed_ssh_users` configuration field.\n\nFor example, the following `config.json` allows access to \"jack123\" and \"alice321\" GitHub users with a SSH user called \"runner\".\n\n```json\n{\n  \"allowed_ssh_users\": [\"runner\"],\n  \"authorized_github_users\": [\"jack123\", \"alice321\"]\n}\n```\n\n### GitHub-based authentication (via OIDC)\n\n`breakpoint` is able to request a fresh GitHub-emitted workflow identifying token, that it sends to `rendezvous`.\n\n`rendezvous` has the ability to verify these, and performs access control based on the repository where the invocation was originated.\n\nEven if no access control is enforced, repository information is logged by `rendezvous` if available.\n\n## Using Namespace's shared Rendezvous\n\nNamespace Labs runs a public `rendezvous` server that is open to everyone. But you can also run your own (see below).\n\nAlthough `rendezvous` facilitates pushing bytes to workloads running in workers (which would otherwise not be able to offer services), the bytes it proxies are not cleartext. Breakpoint establishes end-to-end ssh sessions.\n\nTo use the shared `rendezvous`, use the following endpoint:\n\n```json\n{\n  \"endpoint\": \"rendezvous.namespace.so:5000\"\n}\n```\n\n## Running Rendezvous yourself\n\nSee our [documentation](docs/server-setup.md) on how to run your own instance of `rendezvous`.\n\n## Roadmap\n\nHere's a list of features that we'd to tackle but haven't gotten to yet.\n\n1. Traffic rate limiting: neither the Rendezvous Server nor the Breakpoint client restrict network traffic that is proxied. So far this hasn't been an issue because GitHub runners themselves are network capped.\n2. The Rendezvous Server does not implement a control and monitoring Web UI.\n3. Neither the Rendezvous Server nor the Breakpoint client expose metrics.\n4. The Breakpoint session does not automatically extend itself if an SSH connection is active. You need to explicitly extend the session with `breakpoint extend`.\n5. Configurable ACLs on the Rendezvous Server to specify the list of repositories and organizations allowed to connect to the server.\n6. Support for more authentication schemes between `breakpoint` and `rendezvous`. Breakpoint client and Rendezvous Server only support GitHub's OIDC-based authentication today.\n7. Team and Organization authorization of users in Breakpoint client's SSH service (i.e. specifying a team or org rather than individual usernames).\n\n## Contributions\n\nBreakpoint welcomes your help! We appreciate your time and effort.\n\nIf you find an issue in Breakpoint or you see a missing feature, feel free to open an [Issue](https://github.com/namespacelabs/breakpoint/issues) on GitHub.\n\nCheck out our [contribution guidelines](docs/CONTRIBUTING.md) for more details on how to develop Breakpoint.\n\n## Join the Community\n\nIf you have questions, ideas or feedback, chat with the team on our [Discord server](https://community.namespace.so/discord).\n"
  },
  {
    "path": "api/private/v1/configtype.go",
    "content": "package v1\n\ntype WaitConfig struct {\n\tEndpoint              string    `json:\"endpoint\"`\n\tDuration              string    `json:\"duration\"`\n\tAuthorizedKeys        []string  `json:\"authorized_keys\"`\n\tAuthorizedGithubUsers []string  `json:\"authorized_github_users\"`\n\tShell                 []string  `json:\"shell\"`\n\tAllowedSSHUsers       []string  `json:\"allowed_ssh_users\"`\n\tEnable                []string  `json:\"enable\"`\n\tWebhooks              []Webhook `json:\"webhooks\"`\n\tSlackBot              *SlackBot `json:\"slack_bot\"`\n}\n\ntype Webhook struct {\n\tURL     string         `json:\"url\"`\n\tPayload map[string]any `json:\"payload\"`\n}\n\ntype SlackBot struct {\n\tToken   string `json:\"token\"`\n\tChannel string `json:\"channel\"`\n}\n"
  },
  {
    "path": "api/private/v1/service.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.29.1\n// \tprotoc        (unknown)\n// source: api/private/v1/service.proto\n\npackage v1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\tdurationpb \"google.golang.org/protobuf/types/known/durationpb\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype ExtendRequest struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tWaitFor *durationpb.Duration `protobuf:\"bytes,1,opt,name=wait_for,json=waitFor,proto3\" json:\"wait_for,omitempty\"`\n}\n\nfunc (x *ExtendRequest) Reset() {\n\t*x = ExtendRequest{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_api_private_v1_service_proto_msgTypes[0]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *ExtendRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ExtendRequest) ProtoMessage() {}\n\nfunc (x *ExtendRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_private_v1_service_proto_msgTypes[0]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ExtendRequest.ProtoReflect.Descriptor instead.\nfunc (*ExtendRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_private_v1_service_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *ExtendRequest) GetWaitFor() *durationpb.Duration {\n\tif x != nil {\n\t\treturn x.WaitFor\n\t}\n\treturn nil\n}\n\ntype ExtendResponse struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tExpiration *timestamppb.Timestamp `protobuf:\"bytes,1,opt,name=expiration,proto3\" json:\"expiration,omitempty\"`\n}\n\nfunc (x *ExtendResponse) Reset() {\n\t*x = ExtendResponse{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_api_private_v1_service_proto_msgTypes[1]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *ExtendResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ExtendResponse) ProtoMessage() {}\n\nfunc (x *ExtendResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_private_v1_service_proto_msgTypes[1]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ExtendResponse.ProtoReflect.Descriptor instead.\nfunc (*ExtendResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_private_v1_service_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *ExtendResponse) GetExpiration() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Expiration\n\t}\n\treturn nil\n}\n\ntype StatusResponse struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tExpiration     *timestamppb.Timestamp `protobuf:\"bytes,1,opt,name=expiration,proto3\" json:\"expiration,omitempty\"`\n\tEndpoint       string                 `protobuf:\"bytes,2,opt,name=endpoint,proto3\" json:\"endpoint,omitempty\"`\n\tNumConnections uint32                 `protobuf:\"varint,3,opt,name=num_connections,json=numConnections,proto3\" json:\"num_connections,omitempty\"`\n}\n\nfunc (x *StatusResponse) Reset() {\n\t*x = StatusResponse{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_api_private_v1_service_proto_msgTypes[2]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *StatusResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*StatusResponse) ProtoMessage() {}\n\nfunc (x *StatusResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_private_v1_service_proto_msgTypes[2]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use StatusResponse.ProtoReflect.Descriptor instead.\nfunc (*StatusResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_private_v1_service_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *StatusResponse) GetExpiration() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Expiration\n\t}\n\treturn nil\n}\n\nfunc (x *StatusResponse) GetEndpoint() string {\n\tif x != nil {\n\t\treturn x.Endpoint\n\t}\n\treturn \"\"\n}\n\nfunc (x *StatusResponse) GetNumConnections() uint32 {\n\tif x != nil {\n\t\treturn x.NumConnections\n\t}\n\treturn 0\n}\n\nvar File_api_private_v1_service_proto protoreflect.FileDescriptor\n\nvar file_api_private_v1_service_proto_rawDesc = []byte{\n\t0x0a, 0x1c, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31,\n\t0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x20,\n\t0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x6c, 0x61, 0x62, 0x73, 0x2e, 0x62, 0x72,\n\t0x65, 0x61, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65,\n\t0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,\n\t0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,\n\t0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,\n\t0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67,\n\t0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74,\n\t0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x45,\n\t0x0a, 0x0d, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,\n\t0x34, 0x0a, 0x08, 0x77, 0x61, 0x69, 0x74, 0x5f, 0x66, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28,\n\t0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,\n\t0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x77, 0x61,\n\t0x69, 0x74, 0x46, 0x6f, 0x72, 0x22, 0x4c, 0x0a, 0x0e, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x52,\n\t0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72,\n\t0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,\n\t0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,\n\t0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74,\n\t0x69, 0x6f, 0x6e, 0x22, 0x91, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65,\n\t0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61,\n\t0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,\n\t0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,\n\t0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69,\n\t0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x02,\n\t0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x27,\n\t0x0a, 0x0f, 0x6e, 0x75, 0x6d, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e,\n\t0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x6e, 0x75, 0x6d, 0x43, 0x6f, 0x6e, 0x6e,\n\t0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x8b, 0x02, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x74,\n\t0x72, 0x6f, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x38, 0x0a, 0x06, 0x52, 0x65,\n\t0x73, 0x75, 0x6d, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,\n\t0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67,\n\t0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,\n\t0x6d, 0x70, 0x74, 0x79, 0x12, 0x6b, 0x0a, 0x06, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x12, 0x2f,\n\t0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x6c, 0x61, 0x62, 0x73, 0x2e, 0x62,\n\t0x72, 0x65, 0x61, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74,\n\t0x65, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,\n\t0x30, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x6c, 0x61, 0x62, 0x73, 0x2e,\n\t0x62, 0x72, 0x65, 0x61, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61,\n\t0x74, 0x65, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,\n\t0x65, 0x12, 0x52, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f,\n\t0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,\n\t0x70, 0x74, 0x79, 0x1a, 0x30, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x6c,\n\t0x61, 0x62, 0x73, 0x2e, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x70,\n\t0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73,\n\t0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2d, 0x5a, 0x2b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61,\n\t0x63, 0x65, 0x6c, 0x61, 0x62, 0x73, 0x2e, 0x64, 0x65, 0x76, 0x2f, 0x62, 0x72, 0x65, 0x61, 0x6b,\n\t0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74,\n\t0x65, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,\n}\n\nvar (\n\tfile_api_private_v1_service_proto_rawDescOnce sync.Once\n\tfile_api_private_v1_service_proto_rawDescData = file_api_private_v1_service_proto_rawDesc\n)\n\nfunc file_api_private_v1_service_proto_rawDescGZIP() []byte {\n\tfile_api_private_v1_service_proto_rawDescOnce.Do(func() {\n\t\tfile_api_private_v1_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_private_v1_service_proto_rawDescData)\n\t})\n\treturn file_api_private_v1_service_proto_rawDescData\n}\n\nvar file_api_private_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 3)\nvar file_api_private_v1_service_proto_goTypes = []interface{}{\n\t(*ExtendRequest)(nil),         // 0: namespacelabs.breakpoint.private.ExtendRequest\n\t(*ExtendResponse)(nil),        // 1: namespacelabs.breakpoint.private.ExtendResponse\n\t(*StatusResponse)(nil),        // 2: namespacelabs.breakpoint.private.StatusResponse\n\t(*durationpb.Duration)(nil),   // 3: google.protobuf.Duration\n\t(*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp\n\t(*emptypb.Empty)(nil),         // 5: google.protobuf.Empty\n}\nvar file_api_private_v1_service_proto_depIdxs = []int32{\n\t3, // 0: namespacelabs.breakpoint.private.ExtendRequest.wait_for:type_name -> google.protobuf.Duration\n\t4, // 1: namespacelabs.breakpoint.private.ExtendResponse.expiration:type_name -> google.protobuf.Timestamp\n\t4, // 2: namespacelabs.breakpoint.private.StatusResponse.expiration:type_name -> google.protobuf.Timestamp\n\t5, // 3: namespacelabs.breakpoint.private.ControlService.Resume:input_type -> google.protobuf.Empty\n\t0, // 4: namespacelabs.breakpoint.private.ControlService.Extend:input_type -> namespacelabs.breakpoint.private.ExtendRequest\n\t5, // 5: namespacelabs.breakpoint.private.ControlService.Status:input_type -> google.protobuf.Empty\n\t5, // 6: namespacelabs.breakpoint.private.ControlService.Resume:output_type -> google.protobuf.Empty\n\t1, // 7: namespacelabs.breakpoint.private.ControlService.Extend:output_type -> namespacelabs.breakpoint.private.ExtendResponse\n\t2, // 8: namespacelabs.breakpoint.private.ControlService.Status:output_type -> namespacelabs.breakpoint.private.StatusResponse\n\t6, // [6:9] is the sub-list for method output_type\n\t3, // [3:6] is the sub-list for method input_type\n\t3, // [3:3] is the sub-list for extension type_name\n\t3, // [3:3] is the sub-list for extension extendee\n\t0, // [0:3] is the sub-list for field type_name\n}\n\nfunc init() { file_api_private_v1_service_proto_init() }\nfunc file_api_private_v1_service_proto_init() {\n\tif File_api_private_v1_service_proto != nil {\n\t\treturn\n\t}\n\tif !protoimpl.UnsafeEnabled {\n\t\tfile_api_private_v1_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*ExtendRequest); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_api_private_v1_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*ExtendResponse); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_api_private_v1_service_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*StatusResponse); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: file_api_private_v1_service_proto_rawDesc,\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   3,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_api_private_v1_service_proto_goTypes,\n\t\tDependencyIndexes: file_api_private_v1_service_proto_depIdxs,\n\t\tMessageInfos:      file_api_private_v1_service_proto_msgTypes,\n\t}.Build()\n\tFile_api_private_v1_service_proto = out.File\n\tfile_api_private_v1_service_proto_rawDesc = nil\n\tfile_api_private_v1_service_proto_goTypes = nil\n\tfile_api_private_v1_service_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "api/private/v1/service.proto",
    "content": "syntax = \"proto3\";\n\npackage namespacelabs.breakpoint.private;\n\nimport \"google/protobuf/duration.proto\";\nimport \"google/protobuf/empty.proto\";\nimport \"google/protobuf/timestamp.proto\";\n\noption go_package = \"namespacelabs.dev/breakpoint/api/private/v1\";\n\nservice ControlService {\n  rpc Resume(google.protobuf.Empty) returns (google.protobuf.Empty);\n  rpc Extend(ExtendRequest) returns (ExtendResponse);\n  rpc Status(google.protobuf.Empty) returns (StatusResponse);\n}\n\nmessage ExtendRequest {\n  google.protobuf.Duration wait_for = 1;\n}\n\nmessage ExtendResponse {\n  google.protobuf.Timestamp expiration = 1;\n}\n\nmessage StatusResponse {\n    google.protobuf.Timestamp expiration      = 1;\n    string                    endpoint        = 2;\n    uint32                    num_connections = 3;\n}\n"
  },
  {
    "path": "api/private/v1/service_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.3.0\n// - protoc             (unknown)\n// source: api/private/v1/service.proto\n\npackage v1\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.32.0 or later.\nconst _ = grpc.SupportPackageIsVersion7\n\nconst (\n\tControlService_Resume_FullMethodName = \"/namespacelabs.breakpoint.private.ControlService/Resume\"\n\tControlService_Extend_FullMethodName = \"/namespacelabs.breakpoint.private.ControlService/Extend\"\n\tControlService_Status_FullMethodName = \"/namespacelabs.breakpoint.private.ControlService/Status\"\n)\n\n// ControlServiceClient is the client API for ControlService service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype ControlServiceClient interface {\n\tResume(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\tExtend(ctx context.Context, in *ExtendRequest, opts ...grpc.CallOption) (*ExtendResponse, error)\n\tStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StatusResponse, error)\n}\n\ntype controlServiceClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewControlServiceClient(cc grpc.ClientConnInterface) ControlServiceClient {\n\treturn &controlServiceClient{cc}\n}\n\nfunc (c *controlServiceClient) Resume(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, ControlService_Resume_FullMethodName, in, out, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *controlServiceClient) Extend(ctx context.Context, in *ExtendRequest, opts ...grpc.CallOption) (*ExtendResponse, error) {\n\tout := new(ExtendResponse)\n\terr := c.cc.Invoke(ctx, ControlService_Extend_FullMethodName, in, out, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *controlServiceClient) Status(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StatusResponse, error) {\n\tout := new(StatusResponse)\n\terr := c.cc.Invoke(ctx, ControlService_Status_FullMethodName, in, out, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// ControlServiceServer is the server API for ControlService service.\n// All implementations must embed UnimplementedControlServiceServer\n// for forward compatibility\ntype ControlServiceServer interface {\n\tResume(context.Context, *emptypb.Empty) (*emptypb.Empty, error)\n\tExtend(context.Context, *ExtendRequest) (*ExtendResponse, error)\n\tStatus(context.Context, *emptypb.Empty) (*StatusResponse, error)\n\tmustEmbedUnimplementedControlServiceServer()\n}\n\n// UnimplementedControlServiceServer must be embedded to have forward compatible implementations.\ntype UnimplementedControlServiceServer struct {\n}\n\nfunc (UnimplementedControlServiceServer) Resume(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method Resume not implemented\")\n}\nfunc (UnimplementedControlServiceServer) Extend(context.Context, *ExtendRequest) (*ExtendResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method Extend not implemented\")\n}\nfunc (UnimplementedControlServiceServer) Status(context.Context, *emptypb.Empty) (*StatusResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method Status not implemented\")\n}\nfunc (UnimplementedControlServiceServer) mustEmbedUnimplementedControlServiceServer() {}\n\n// UnsafeControlServiceServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to ControlServiceServer will\n// result in compilation errors.\ntype UnsafeControlServiceServer interface {\n\tmustEmbedUnimplementedControlServiceServer()\n}\n\nfunc RegisterControlServiceServer(s grpc.ServiceRegistrar, srv ControlServiceServer) {\n\ts.RegisterService(&ControlService_ServiceDesc, srv)\n}\n\nfunc _ControlService_Resume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(ControlServiceServer).Resume(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: ControlService_Resume_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(ControlServiceServer).Resume(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _ControlService_Extend_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ExtendRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(ControlServiceServer).Extend(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: ControlService_Extend_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(ControlServiceServer).Extend(ctx, req.(*ExtendRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _ControlService_Status_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(ControlServiceServer).Status(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: ControlService_Status_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(ControlServiceServer).Status(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// ControlService_ServiceDesc is the grpc.ServiceDesc for ControlService service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar ControlService_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"namespacelabs.breakpoint.private.ControlService\",\n\tHandlerType: (*ControlServiceServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"Resume\",\n\t\t\tHandler:    _ControlService_Resume_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"Extend\",\n\t\t\tHandler:    _ControlService_Extend_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"Status\",\n\t\t\tHandler:    _ControlService_Status_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"api/private/v1/service.proto\",\n}\n"
  },
  {
    "path": "api/public/v1/service.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.29.1\n// \tprotoc        (unknown)\n// source: api/public/v1/service.proto\n\npackage v1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype RegisterRequest struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n}\n\nfunc (x *RegisterRequest) Reset() {\n\t*x = RegisterRequest{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_api_public_v1_service_proto_msgTypes[0]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *RegisterRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RegisterRequest) ProtoMessage() {}\n\nfunc (x *RegisterRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_public_v1_service_proto_msgTypes[0]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RegisterRequest.ProtoReflect.Descriptor instead.\nfunc (*RegisterRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_public_v1_service_proto_rawDescGZIP(), []int{0}\n}\n\ntype RegisterResponse struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tEndpoint string `protobuf:\"bytes,1,opt,name=endpoint,proto3\" json:\"endpoint,omitempty\"` // Connection endpoint, e.g. <address>:<port>\n}\n\nfunc (x *RegisterResponse) Reset() {\n\t*x = RegisterResponse{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_api_public_v1_service_proto_msgTypes[1]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *RegisterResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RegisterResponse) ProtoMessage() {}\n\nfunc (x *RegisterResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_public_v1_service_proto_msgTypes[1]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RegisterResponse.ProtoReflect.Descriptor instead.\nfunc (*RegisterResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_public_v1_service_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *RegisterResponse) GetEndpoint() string {\n\tif x != nil {\n\t\treturn x.Endpoint\n\t}\n\treturn \"\"\n}\n\nvar File_api_public_v1_service_proto protoreflect.FileDescriptor\n\nvar file_api_public_v1_service_proto_rawDesc = []byte{\n\t0x0a, 0x1b, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2f, 0x76, 0x31, 0x2f,\n\t0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x18, 0x6e,\n\t0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x6c, 0x61, 0x62, 0x73, 0x2e, 0x62, 0x72, 0x65,\n\t0x61, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x11, 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x73,\n\t0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2e, 0x0a, 0x10, 0x52, 0x65,\n\t0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a,\n\t0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,\n\t0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x32, 0x73, 0x0a, 0x0c, 0x50, 0x72,\n\t0x6f, 0x78, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x08, 0x52, 0x65,\n\t0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x29, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61,\n\t0x63, 0x65, 0x6c, 0x61, 0x62, 0x73, 0x2e, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x70, 0x6f, 0x69, 0x6e,\n\t0x74, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,\n\t0x74, 0x1a, 0x2a, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x6c, 0x61, 0x62,\n\t0x73, 0x2e, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x67,\n\t0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42,\n\t0x2c, 0x5a, 0x2a, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x6c, 0x61, 0x62, 0x73,\n\t0x2e, 0x64, 0x65, 0x76, 0x2f, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2f,\n\t0x61, 0x70, 0x69, 0x2f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70,\n\t0x72, 0x6f, 0x74, 0x6f, 0x33,\n}\n\nvar (\n\tfile_api_public_v1_service_proto_rawDescOnce sync.Once\n\tfile_api_public_v1_service_proto_rawDescData = file_api_public_v1_service_proto_rawDesc\n)\n\nfunc file_api_public_v1_service_proto_rawDescGZIP() []byte {\n\tfile_api_public_v1_service_proto_rawDescOnce.Do(func() {\n\t\tfile_api_public_v1_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_public_v1_service_proto_rawDescData)\n\t})\n\treturn file_api_public_v1_service_proto_rawDescData\n}\n\nvar file_api_public_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 2)\nvar file_api_public_v1_service_proto_goTypes = []interface{}{\n\t(*RegisterRequest)(nil),  // 0: namespacelabs.breakpoint.RegisterRequest\n\t(*RegisterResponse)(nil), // 1: namespacelabs.breakpoint.RegisterResponse\n}\nvar file_api_public_v1_service_proto_depIdxs = []int32{\n\t0, // 0: namespacelabs.breakpoint.ProxyService.Register:input_type -> namespacelabs.breakpoint.RegisterRequest\n\t1, // 1: namespacelabs.breakpoint.ProxyService.Register:output_type -> namespacelabs.breakpoint.RegisterResponse\n\t1, // [1:2] is the sub-list for method output_type\n\t0, // [0:1] is the sub-list for method input_type\n\t0, // [0:0] is the sub-list for extension type_name\n\t0, // [0:0] is the sub-list for extension extendee\n\t0, // [0:0] is the sub-list for field type_name\n}\n\nfunc init() { file_api_public_v1_service_proto_init() }\nfunc file_api_public_v1_service_proto_init() {\n\tif File_api_public_v1_service_proto != nil {\n\t\treturn\n\t}\n\tif !protoimpl.UnsafeEnabled {\n\t\tfile_api_public_v1_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*RegisterRequest); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_api_public_v1_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*RegisterResponse); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: file_api_public_v1_service_proto_rawDesc,\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   2,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_api_public_v1_service_proto_goTypes,\n\t\tDependencyIndexes: file_api_public_v1_service_proto_depIdxs,\n\t\tMessageInfos:      file_api_public_v1_service_proto_msgTypes,\n\t}.Build()\n\tFile_api_public_v1_service_proto = out.File\n\tfile_api_public_v1_service_proto_rawDesc = nil\n\tfile_api_public_v1_service_proto_goTypes = nil\n\tfile_api_public_v1_service_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "api/public/v1/service.proto",
    "content": "syntax = \"proto3\";\n\npackage namespacelabs.breakpoint;\n\noption go_package = \"namespacelabs.dev/breakpoint/api/public/v1\";\n\nservice ProxyService {\n  // The reverse tunnel is active for as long as this stream over a quic connection is active.\n  rpc Register(RegisterRequest) returns (stream RegisterResponse);\n}\n\nmessage RegisterRequest {}\n\nmessage RegisterResponse {\n  string endpoint = 1; // Connection endpoint, e.g. <address>:<port>\n}\n"
  },
  {
    "path": "api/public/v1/service_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.3.0\n// - protoc             (unknown)\n// source: api/public/v1/service.proto\n\npackage v1\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.32.0 or later.\nconst _ = grpc.SupportPackageIsVersion7\n\nconst (\n\tProxyService_Register_FullMethodName = \"/namespacelabs.breakpoint.ProxyService/Register\"\n)\n\n// ProxyServiceClient is the client API for ProxyService service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype ProxyServiceClient interface {\n\t// The reverse tunnel is active for as long as this stream over a quic connection is active.\n\tRegister(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (ProxyService_RegisterClient, error)\n}\n\ntype proxyServiceClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewProxyServiceClient(cc grpc.ClientConnInterface) ProxyServiceClient {\n\treturn &proxyServiceClient{cc}\n}\n\nfunc (c *proxyServiceClient) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (ProxyService_RegisterClient, error) {\n\tstream, err := c.cc.NewStream(ctx, &ProxyService_ServiceDesc.Streams[0], ProxyService_Register_FullMethodName, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &proxyServiceRegisterClient{stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\ntype ProxyService_RegisterClient interface {\n\tRecv() (*RegisterResponse, error)\n\tgrpc.ClientStream\n}\n\ntype proxyServiceRegisterClient struct {\n\tgrpc.ClientStream\n}\n\nfunc (x *proxyServiceRegisterClient) Recv() (*RegisterResponse, error) {\n\tm := new(RegisterResponse)\n\tif err := x.ClientStream.RecvMsg(m); err != nil {\n\t\treturn nil, err\n\t}\n\treturn m, nil\n}\n\n// ProxyServiceServer is the server API for ProxyService service.\n// All implementations must embed UnimplementedProxyServiceServer\n// for forward compatibility\ntype ProxyServiceServer interface {\n\t// The reverse tunnel is active for as long as this stream over a quic connection is active.\n\tRegister(*RegisterRequest, ProxyService_RegisterServer) error\n\tmustEmbedUnimplementedProxyServiceServer()\n}\n\n// UnimplementedProxyServiceServer must be embedded to have forward compatible implementations.\ntype UnimplementedProxyServiceServer struct {\n}\n\nfunc (UnimplementedProxyServiceServer) Register(*RegisterRequest, ProxyService_RegisterServer) error {\n\treturn status.Errorf(codes.Unimplemented, \"method Register not implemented\")\n}\nfunc (UnimplementedProxyServiceServer) mustEmbedUnimplementedProxyServiceServer() {}\n\n// UnsafeProxyServiceServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to ProxyServiceServer will\n// result in compilation errors.\ntype UnsafeProxyServiceServer interface {\n\tmustEmbedUnimplementedProxyServiceServer()\n}\n\nfunc RegisterProxyServiceServer(s grpc.ServiceRegistrar, srv ProxyServiceServer) {\n\ts.RegisterService(&ProxyService_ServiceDesc, srv)\n}\n\nfunc _ProxyService_Register_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(RegisterRequest)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(ProxyServiceServer).Register(m, &proxyServiceRegisterServer{stream})\n}\n\ntype ProxyService_RegisterServer interface {\n\tSend(*RegisterResponse) error\n\tgrpc.ServerStream\n}\n\ntype proxyServiceRegisterServer struct {\n\tgrpc.ServerStream\n}\n\nfunc (x *proxyServiceRegisterServer) Send(m *RegisterResponse) error {\n\treturn x.ServerStream.SendMsg(m)\n}\n\n// ProxyService_ServiceDesc is the grpc.ServiceDesc for ProxyService service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar ProxyService_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"namespacelabs.breakpoint.ProxyService\",\n\tHandlerType: (*ProxyServiceServer)(nil),\n\tMethods:     []grpc.MethodDesc{},\n\tStreams: []grpc.StreamDesc{\n\t\t{\n\t\t\tStreamName:    \"Register\",\n\t\t\tHandler:       _ProxyService_Register_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t},\n\tMetadata: \"api/public/v1/service.proto\",\n}\n"
  },
  {
    "path": "api/public/v1/types.go",
    "content": "package v1\n\nconst (\n\tQuicProto = \"breakpoint-grpc\"\n\n\tGitHubOIDCTokenHeader = \"x-breakpoint-github-oidc-token\"\n\n\tGitHubOIDCAudience = \"namespacelabs.dev/breakpoint\"\n)\n"
  },
  {
    "path": "buf.gen.yaml",
    "content": "version: v1\nplugins:\n  - plugin: go\n    out: .\n    opt: paths=source_relative\n  - plugin: go-grpc\n    out: .\n    opt: paths=source_relative\n"
  },
  {
    "path": "cmd/breakpoint/attach.go",
    "content": "package main\n\nimport (\n\t\"errors\"\n\t\"net\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/spf13/cobra\"\n\t\"inet.af/tcpproxy\"\n\t\"namespacelabs.dev/breakpoint/pkg/quicproxyclient\"\n)\n\nfunc newAttachCmd() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse: \"attach\",\n\t}\n\n\tendpoint := cmd.Flags().String(\"endpoint\", \"\", \"The address of the server.\")\n\ttarget := cmd.Flags().String(\"target\", \"\", \"Where to connect to.\")\n\n\tcmd.RunE = func(cmd *cobra.Command, args []string) error {\n\t\tif *endpoint == \"\" {\n\t\t\treturn errors.New(\"--endpoint is required\")\n\t\t}\n\n\t\tif *target == \"\" {\n\t\t\treturn errors.New(\"--target is required\")\n\t\t}\n\n\t\treturn quicproxyclient.Serve(cmd.Context(), *endpoint, nil, quicproxyclient.Handlers{\n\t\t\tOnAllocation: func(endpoint string) {\n\t\t\t\tzerolog.Ctx(cmd.Context()).Info().Str(\"endpoint\", endpoint).Msg(\"Got allocation\")\n\t\t\t},\n\t\t\tProxy: func(conn net.Conn) error {\n\t\t\t\tzerolog.Ctx(cmd.Context()).Info().Str(\"target\", *target).Msg(\"handling reverse proxy\")\n\t\t\t\tgo tcpproxy.To(*target).HandleConn(conn)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t})\n\t}\n\n\treturn cmd\n}\n\nfunc init() {\n\trootCmd.AddCommand(newAttachCmd())\n}\n"
  },
  {
    "path": "cmd/breakpoint/extend.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\t\"google.golang.org/protobuf/types/known/durationpb\"\n\tpb \"namespacelabs.dev/breakpoint/api/private/v1\"\n\t\"namespacelabs.dev/breakpoint/pkg/bcontrol\"\n\t\"namespacelabs.dev/breakpoint/pkg/waiter\"\n\n\t\"github.com/dustin/go-humanize\"\n)\n\nfunc newExtendCmd() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"extend\",\n\t\tShort: \"Extend the breakpoint duration.\",\n\t}\n\n\textendWaitFor := cmd.Flags().Duration(\"for\", time.Minute*30, \"How much to extend the breakpoint by.\")\n\textendWaitDuration := cmd.Flags().Duration(\"duration\", 0, \"Alias of --for\")\n\tcmd.MarkFlagsMutuallyExclusive(\"duration\", \"for\")\n\n\tcmd.RunE = func(cmd *cobra.Command, args []string) error {\n\t\tduration := *extendWaitDuration\n\t\tif *extendWaitDuration == 0 {\n\t\t\tduration = *extendWaitFor\n\t\t}\n\n\t\tif duration <= 0 {\n\t\t\treturn fmt.Errorf(\"duration must be positive\")\n\t\t}\n\n\t\tclt, conn, err := bcontrol.Connect(cmd.Context())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdefer conn.Close()\n\n\t\tresp, err := clt.Extend(cmd.Context(), &pb.ExtendRequest{\n\t\t\tWaitFor: durationpb.New(duration),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\texpiration := resp.Expiration.AsTime()\n\t\tfmt.Printf(\"Breakpoint now expires at %s (%s)\\n\",\n\t\t\texpiration.Format(waiter.Stamp),\n\t\t\thumanize.Time(expiration))\n\n\t\treturn nil\n\t}\n\n\treturn cmd\n}\n\nfunc init() {\n\trootCmd.AddCommand(newExtendCmd())\n}\n"
  },
  {
    "path": "cmd/breakpoint/hold.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/spf13/cobra\"\n\t\"google.golang.org/protobuf/types/known/durationpb\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\tv1 \"namespacelabs.dev/breakpoint/api/private/v1\"\n\t\"namespacelabs.dev/breakpoint/pkg/bcontrol\"\n\t\"namespacelabs.dev/breakpoint/pkg/waiter\"\n)\n\nfunc init() {\n\trootCmd.AddCommand(newHoldCmd())\n}\n\nconst (\n\textendBy = 30 * time.Second\n)\n\nfunc newHoldCmd() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"hold\",\n\t\tShort: \"Holds until a breakpoint is finished or for a certain amount of time.\",\n\t}\n\n\tholdFor := cmd.Flags().Duration(\"for\", time.Minute*30, \"How much to extend the breakpoint by.\")\n\tholdDuration := cmd.Flags().Duration(\"duration\", 0, \"Alias of --for\")\n\tshouldHoldWhileConnected := cmd.Flags().Bool(\"while-connected\", false, \"Keep holding while there are active connections, even after duration has passed\")\n\tstopWhenDone := cmd.Flags().Bool(\"stop\", false, \"Stop the breakpoint server after holding\")\n\tcmd.MarkFlagsMutuallyExclusive(\"duration\", \"for\", \"while-connected\")\n\n\tcmd.RunE = func(cmd *cobra.Command, args []string) error {\n\t\tduration := *holdDuration\n\t\tif *holdDuration == 0 {\n\t\t\tduration = *holdFor\n\t\t}\n\n\t\tctx := cmd.Context()\n\t\tif *shouldHoldWhileConnected {\n\t\t\tif err := holdWhileConnected(ctx); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tif err := holdForDuration(ctx, duration); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif *stopWhenDone {\n\t\t\tif err := stopBreakpoint(ctx); err != nil {\n\t\t\t\tfmt.Printf(\"Failed to stop breakpoint: %v\\n\", err)\n\t\t\t} else {\n\t\t\t\tfmt.Printf(\"Stopped breakpoint\\n\")\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn cmd\n}\n\nfunc holdForDuration(ctx context.Context, duration time.Duration) error {\n\tif duration <= 0 {\n\t\treturn fmt.Errorf(\"duration must be positive\")\n\t}\n\n\tstatus, err := getStatus(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\twaiter.PrintConnectionInfo(status.Endpoint, status.Expiration.AsTime(), os.Stderr)\n\n\tfmt.Printf(\"Holding until %s\\n\", humanize.Time(time.Now().Add(duration)))\n\n\ttimer := time.NewTimer(duration)\n\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-timer.C:\n\t\treturn nil\n\t}\n}\n\nfunc holdWhileConnected(ctx context.Context) error {\n\tclt, conn, err := bcontrol.Connect(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer conn.Close()\n\n\tstatus, err := clt.Status(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to fetch breakpoint status, is breakpoint running\")\n\t}\n\n\tif status.GetNumConnections() < 1 {\n\t\tfmt.Printf(\"No active connections, exiting\\n\")\n\t\treturn nil\n\t}\n\n\twaiter.PrintConnectionInfo(status.Endpoint, status.Expiration.AsTime(), os.Stderr)\n\n\ttickDuration := 5 * time.Second\n\tticker := time.NewTicker(tickDuration)\n\tdefer ticker.Stop()\n\n\tfmt.Printf(\"Waiting until breakpoint has no active connections\\n\")\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\n\t\tcase <-ticker.C:\n\t\t\tstatus, err := clt.Status(ctx, &emptypb.Empty{})\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to fetch breakpoint status, assuming no longer running\")\n\t\t\t}\n\n\t\t\texpiration := status.GetExpiration().AsTime()\n\t\t\tif !expiration.IsZero() && time.Now().Add(2*tickDuration).After(expiration) {\n\t\t\t\ttryExtendBreakpoint(ctx, expiration, clt)\n\t\t\t}\n\n\t\t\tif status.GetNumConnections() > 0 {\n\t\t\t\tfmt.Printf(\"Active connections: %d, waiting\\n\", status.GetNumConnections())\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"No active connections, exiting\\n\")\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc tryExtendBreakpoint(ctx context.Context, currentExpiration time.Time, clt v1.ControlServiceClient) {\n\tfmt.Printf(\"Breakpoint expiring %s, extending by %s\\n\", humanize.Time(currentExpiration), extendBy)\n\n\tret, err := clt.Extend(ctx, &v1.ExtendRequest{\n\t\tWaitFor: durationpb.New(extendBy),\n\t})\n\tif err != nil {\n\t\tfmt.Printf(\"Unable to extend breakpoint: %v\\n\", err)\n\t}\n\n\tfmt.Printf(\"Breakpoint now expires %s\\n\", humanize.Time(ret.GetExpiration().AsTime()))\n}\n\nfunc stopBreakpoint(ctx context.Context) error {\n\tclt, conn, err := bcontrol.Connect(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer conn.Close()\n\n\t_, err = clt.Resume(ctx, &emptypb.Empty{})\n\treturn err\n}\n"
  },
  {
    "path": "cmd/breakpoint/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/spf13/cobra\"\n\t\"namespacelabs.dev/breakpoint/pkg/blog\"\n)\n\nvar rootCmd = &cobra.Command{\n\tUse:   \"breakpoint\",\n\tShort: `Add breakpoints to CI workflows.`,\n}\n\nfunc main() {\n\t// This is the only control we have available.\n\tos.Setenv(\"QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING\", \"true\")\n\n\tl := blog.New()\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\terr := rootCmd.ExecuteContext(l.WithContext(ctx))\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, err)\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "cmd/breakpoint/resume.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\t\"namespacelabs.dev/breakpoint/pkg/bcontrol\"\n)\n\nfunc newResumeCmd() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"resume\",\n\t\tShort: \"Resume the workflow execution.\",\n\t}\n\n\tcmd.RunE = func(cmd *cobra.Command, args []string) error {\n\t\tclt, conn, err := bcontrol.Connect(cmd.Context())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdefer conn.Close()\n\n\t\tif _, err := clt.Resume(cmd.Context(), &emptypb.Empty{}); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfmt.Printf(\"Breakpoint removed, workflow resuming!\\n\")\n\t\treturn nil\n\t}\n\n\treturn cmd\n}\n\nfunc init() {\n\trootCmd.AddCommand(newResumeCmd())\n}\n"
  },
  {
    "path": "cmd/breakpoint/start.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\tv1 \"namespacelabs.dev/breakpoint/api/private/v1\"\n\t\"namespacelabs.dev/breakpoint/pkg/bcontrol\"\n\t\"namespacelabs.dev/breakpoint/pkg/execbackground\"\n\t\"namespacelabs.dev/breakpoint/pkg/waiter\"\n)\n\nfunc init() {\n\trootCmd.AddCommand(newStartCmd())\n}\n\nfunc newStartCmd() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"start\",\n\t\tShort: \"Starts breakpoint in the background\",\n\t}\n\n\tconfigPath := cmd.Flags().String(\"config\", \"\", \"Path to the configuration file.\")\n\n\tcmd.RunE = func(cmd *cobra.Command, args []string) error {\n\t\tif *configPath == \"\" {\n\t\t\treturn errors.New(\"--config is required\")\n\t\t}\n\n\t\tprocArgs := []string{\"wait\", \"--config\", *configPath}\n\t\tproc := exec.Command(os.Args[0], procArgs...)\n\t\texecbackground.SetCreateSession(proc)\n\n\t\tif err := proc.Start(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to start background process: %w\", err)\n\t\t}\n\n\t\tpid := proc.Process.Pid\n\n\t\tfmt.Fprintf(os.Stderr, \"Breakpoint starting in background (PID: %d)\\n\", pid)\n\n\t\tstatus, err := waitForReady(cmd.Context(), 5*time.Second)\n\t\tif err != nil {\n\t\t\t_ = proc.Process.Kill()\n\t\t\treturn err\n\t\t}\n\n\t\tif err := proc.Process.Release(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\twaiter.PrintConnectionInfo(status.Endpoint, status.GetExpiration().AsTime(), os.Stderr)\n\n\t\treturn nil\n\t}\n\n\treturn cmd\n}\n\nfunc waitForReady(ctx context.Context, timeoutDuration time.Duration) (*v1.StatusResponse, error) {\n\t// Check for file existence with timeout\n\ttimeout := time.After(timeoutDuration)\n\tticker := time.NewTicker(100 * time.Millisecond)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\n\t\tcase <-timeout:\n\t\t\treturn nil, fmt.Errorf(\"breakpoint didn't start in time\")\n\n\t\tcase <-ticker.C:\n\t\t\tstatus, err := getStatus(ctx)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif status.GetEndpoint() != \"\" {\n\t\t\t\treturn status, nil\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc getStatus(ctx context.Context) (*v1.StatusResponse, error) {\n\tclt, conn, err := bcontrol.Connect(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer conn.Close()\n\n\tstatus, err := clt.Status(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn status, nil\n}\n"
  },
  {
    "path": "cmd/breakpoint/status.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\t\"namespacelabs.dev/breakpoint/pkg/bcontrol\"\n\t\"namespacelabs.dev/breakpoint/pkg/waiter\"\n)\n\nfunc init() {\n\trootCmd.AddCommand(newStatusCmd())\n}\n\nfunc newStatusCmd() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"status\",\n\t\tShort: \"Get the current status of breakpoint\",\n\t}\n\n\tcmd.RunE = func(cmd *cobra.Command, args []string) error {\n\t\tclt, conn, err := bcontrol.Connect(cmd.Context())\n\t\tif err != nil {\n\t\t\tfmt.Fprintln(os.Stderr, err)\n\t\t\tfmt.Fprintln(os.Stdout, \"Unable to connect to breakpoint control server, is breakpoint running?\")\n\t\t\tos.Exit(1)\n\t\t\treturn nil\n\t\t}\n\n\t\tdefer conn.Close()\n\n\t\tstatus, err := clt.Status(cmd.Context(), &emptypb.Empty{})\n\t\tif err != nil {\n\t\t\tfmt.Fprintln(os.Stderr, err)\n\t\t\tfmt.Fprintln(os.Stdout, \"Unable to retrieve status from breakpoint control server, is breakpoint running?\")\n\t\t\tos.Exit(1)\n\t\t\treturn nil\n\t\t}\n\n\t\twaiter.PrintConnectionInfo(status.Endpoint, status.Expiration.AsTime(), os.Stdout)\n\n\t\tfmt.Fprintf(os.Stdout, \"\\nActive connections: %d\\n\", status.GetNumConnections())\n\n\t\treturn nil\n\t}\n\n\treturn cmd\n}\n"
  },
  {
    "path": "cmd/breakpoint/wait.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/muesli/reflow/wordwrap\"\n\t\"github.com/spf13/cobra\"\n\t\"golang.org/x/sync/errgroup\"\n\t\"namespacelabs.dev/breakpoint/pkg/config\"\n\t\"namespacelabs.dev/breakpoint/pkg/internalserver\"\n\t\"namespacelabs.dev/breakpoint/pkg/passthrough\"\n\t\"namespacelabs.dev/breakpoint/pkg/quicproxyclient\"\n\t\"namespacelabs.dev/breakpoint/pkg/sshd\"\n\t\"namespacelabs.dev/breakpoint/pkg/waiter\"\n)\n\nfunc init() {\n\trootCmd.AddCommand(newWaitCmd())\n}\n\nfunc newWaitCmd() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"wait\",\n\t\tShort: \"Blocks for the duration of the breakpoint\",\n\t}\n\n\tconfigPath := cmd.Flags().String(\"config\", \"\", \"Path to the configuration file.\")\n\n\tcmd.RunE = func(cmd *cobra.Command, args []string) error {\n\t\tif *configPath == \"\" {\n\t\t\treturn errors.New(\"--config is required\")\n\t\t}\n\n\t\tctx := cmd.Context()\n\n\t\tcfg, err := config.LoadConfig(ctx, *configPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tmopts := waiter.ManagerOpts{\n\t\t\tInitialDur: cfg.ParsedDuration,\n\t\t\tWebhooks:   cfg.Webhooks,\n\t\t}\n\n\t\tif cfg.SlackBot != nil {\n\t\t\tmopts.SlackBots = append(mopts.SlackBots, *cfg.SlackBot)\n\t\t}\n\n\t\tmgr, ctx := waiter.NewManager(ctx, mopts)\n\n\t\tsshd, err := sshd.MakeServer(ctx, sshd.SSHServerOpts{\n\t\t\tShell:          cfg.Shell,\n\t\t\tAuthorizedKeys: cfg.AllKeys,\n\t\t\tAllowedUsers:   cfg.AllowedSSHUsers,\n\t\t\tEnv:            os.Environ(),\n\t\t\tInteractiveMOTD: func(w io.Writer) {\n\t\t\t\tww := wordwrap.NewWriter(80)\n\n\t\t\t\tfmt.Fprintln(ww)\n\t\t\t\tfmt.Fprintf(ww, \"Welcome to a breakpoint-provided remote shell.\\n\")\n\t\t\t\tfmt.Fprintln(ww)\n\t\t\t\tfmt.Fprintf(ww, \"This breakpoint will expire %s.\\n\", humanize.Time(mgr.Expiration()))\n\t\t\t\tfmt.Fprintln(ww)\n\t\t\t\tfmt.Fprintf(ww, \"The following additional commands are available:\\n\\n\")\n\t\t\t\tfmt.Fprintf(ww, \" - `breakpoint extend` to extend the breakpoint duration.\\n\")\n\t\t\t\tfmt.Fprintf(ww, \" - `breakpoint resume` to resume immediately.\\n\")\n\n\t\t\t\t_ = ww.Close()\n\n\t\t\t\t_, _ = w.Write(ww.Bytes())\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tmgr.SetConnectionCountCallback(sshd.NumConnections)\n\n\t\teg, ctx := errgroup.WithContext(ctx)\n\n\t\tpl := passthrough.NewListener(ctx, dummyAddr{})\n\n\t\teg.Go(func() error {\n\t\t\treturn sshd.Server.Serve(pl)\n\t\t})\n\n\t\teg.Go(func() error {\n\t\t\tdefer pl.Close()\n\n\t\t\treturn quicproxyclient.Serve(ctx, cfg.Endpoint, cfg.RegisterMetadata, quicproxyclient.Handlers{\n\t\t\t\tOnAllocation: func(endpoint string) {\n\t\t\t\t\tmgr.SetEndpoint(endpoint)\n\t\t\t\t},\n\t\t\t\tProxy: pl.Offer,\n\t\t\t})\n\t\t})\n\n\t\teg.Go(func() error {\n\t\t\treturn internalserver.ListenAndServe(ctx, mgr)\n\t\t})\n\n\t\teg.Go(func() error {\n\t\t\treturn mgr.Wait()\n\t\t})\n\n\t\treturn cancelIsOK(eg.Wait())\n\t}\n\n\treturn cmd\n}\n\nfunc cancelIsOK(err error) error {\n\tif errors.Is(err, context.Canceled) {\n\t\treturn nil\n\t}\n\n\treturn err\n}\n\ntype dummyAddr struct{}\n\nfunc (dummyAddr) Network() string { return \"internal\" }\nfunc (dummyAddr) String() string  { return \"quic-revproxy\" }\n"
  },
  {
    "path": "cmd/rendezvous/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"os\"\n\t\"strings\"\n\n\t\"golang.org/x/exp/slices\"\n\t\"golang.org/x/sync/errgroup\"\n\t\"namespacelabs.dev/breakpoint/pkg/blog\"\n\t\"namespacelabs.dev/breakpoint/pkg/quicproxy\"\n\t\"namespacelabs.dev/breakpoint/pkg/tlscerts\"\n)\n\nvar (\n\tlistenOn         = flag.String(\"l\", \"\", \"The address:port to listen on.\")\n\tpublicAddress    = flag.String(\"pub\", \"\", \"If unset, defaults to listen address.\")\n\tsubjectDomains   = flag.String(\"sub\", \"\", \"Attaches the specified domain names as TLS cert subjects.\")\n\tfrontend         = flag.String(\"frontend\", \"\", \"If specified, configures the frontend (in JSON).\")\n\thttpPort         = flag.Int(\"http_port\", 10020, \"Where we listen on HTTP.\")\n\tenableGitHubOIDC = flag.Bool(\"validate_github_oidc\", false, \"Validate GitHub OIDC tokens.\")\n\tredirectTarget   = flag.String(\"redirect_target\", \"https://github.com/namespacelabs/breakpoint\", \"Where to redirect users to when accessed via HTTP.\")\n)\n\ntype frontendConfig struct {\n\tKind       string `json:\"kind\"`\n\tPortStart  int    `json:\"port_start\"`\n\tPortEnd    int    `json:\"port_end\"`\n\tPortListen int    `json:\"listen_port\"`\n}\n\nfunc main() {\n\tflag.Parse()\n\n\tvar fcfg frontendConfig\n\tif frontendData := flagOrEnv(\"PROXY_FRONTEND\", *frontend); frontendData != \"\" {\n\t\tif err := json.Unmarshal([]byte(frontendData), &fcfg); err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t}\n\n\tvar domains []string\n\tif val := flagOrEnv(\"PROXY_DOMAINS\", *subjectDomains); len(val) > 0 {\n\t\tdomains = strings.Split(val, \",\")\n\t}\n\n\tif err := run(Config{\n\t\tListenAddr:       flagOrEnv(\"PROXY_LISTEN\", *listenOn),\n\t\tHttpPort:         *httpPort,\n\t\tFrontendConfig:   fcfg,\n\t\tPublicAddr:       flagOrEnv(\"PROXY_PUBLIC\", *publicAddress),\n\t\tDomains:          domains,\n\t\tEnableGitHubOIDC: flagOrEnvBool(\"PROXY_VALIDATE_GITHUB_OIDC\", *enableGitHubOIDC),\n\t\tRedirectURL:      *redirectTarget,\n\t}); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\nfunc flagOrEnv(env, flag string) string {\n\tif flag != \"\" {\n\t\treturn flag\n\t}\n\n\treturn os.Getenv(env)\n}\n\nfunc flagOrEnvBool(env string, flag bool) bool {\n\treturn flag || os.Getenv(env) == \"true\" || os.Getenv(env) == \"1\"\n}\n\ntype Config struct {\n\tListenAddr       string\n\tHttpPort         int\n\tFrontendConfig   frontendConfig\n\tPublicAddr       string\n\tDomains          []string\n\tEnableGitHubOIDC bool\n\tRedirectURL      string\n}\n\nfunc run(opts Config) error {\n\tif opts.ListenAddr == \"\" {\n\t\treturn errors.New(\"-l or PROXY_LISTEN is required\")\n\t}\n\n\tif opts.PublicAddr == \"\" {\n\t\taddrport, err := netip.ParseAddrPort(opts.ListenAddr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\topts.PublicAddr = addrport.Addr().String()\n\t}\n\n\tsubjects := tlscerts.Subjects{\n\t\tDNSNames: opts.Domains,\n\t}\n\n\tif addr, err := netip.ParseAddr(opts.PublicAddr); err == nil {\n\t\tif !addr.IsUnspecified() {\n\t\t\tsubjects.IPAddresses = append(subjects.IPAddresses, net.IP(addr.AsSlice()))\n\t\t}\n\t} else {\n\t\tif !slices.Contains(subjects.DNSNames, opts.PublicAddr) {\n\t\t\tsubjects.DNSNames = append(subjects.DNSNames, opts.PublicAddr)\n\t\t}\n\t}\n\n\tfrontend := makeFrontend(opts.FrontendConfig, opts.PublicAddr)\n\n\tl := blog.New()\n\tctx := l.WithContext(context.Background())\n\n\tproxy, err := quicproxy.NewServer(ctx, quicproxy.ServerOpts{\n\t\tProxyFrontend:    frontend,\n\t\tListenAddr:       opts.ListenAddr,\n\t\tSubjects:         subjects,\n\t\tEnableGitHubOIDC: opts.EnableGitHubOIDC,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\teg, ctx := errgroup.WithContext(ctx)\n\teg.Go(func() error {\n\t\treturn frontend.ListenAndServe(ctx)\n\t})\n\n\teg.Go(func() error {\n\t\treturn proxy.Serve(ctx)\n\t})\n\n\teg.Go(func() error {\n\t\th := http.NewServeMux()\n\n\t\th.Handle(\"/\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Location\", opts.RedirectURL)\n\t\t\tw.WriteHeader(http.StatusTemporaryRedirect)\n\t\t\tfmt.Fprintf(w, \"Heading over to <a href=%q>%s</a>\", opts.RedirectURL, opts.RedirectURL)\n\t\t}))\n\n\t\treturn http.ListenAndServe(fmt.Sprintf(\":%d\", opts.HttpPort), h)\n\t})\n\n\treturn eg.Wait()\n}\n\nfunc makeFrontend(fcfg frontendConfig, pub string) quicproxy.ProxyFrontend {\n\tswitch fcfg.Kind {\n\tcase \"proxy_proto\":\n\t\treturn &quicproxy.ProxyProtoFrontend{\n\t\t\tListenPort: fcfg.PortListen,\n\t\t\tPortStart:  fcfg.PortStart,\n\t\t\tPortEnd:    fcfg.PortEnd,\n\t\t\tPublicAddr: pub,\n\t\t}\n\n\tdefault:\n\t\treturn quicproxy.RawFrontend{\n\t\t\tPublicAddr: pub,\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "docs/CONTRIBUTING.md",
    "content": "# Contributing to Breakpoint\n\n## Where to Start\n\nYou can find good issues to tackle with labels [`good first issue`](https://github.com/namespacelabs/breakpoint/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and [`help wanted`](https://github.com/namespacelabs/breakpoint/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22).\n\n## Issues tracking and Pull Requests\n\nWe don't enforce any rigid contributing procedure. We appreciate you spending time improving `breakpoint`!\n\nIf in doubt, please open a [new Issue](https://github.com/namespacelabs/breakpoint/issues/new) on GitHub. One of the maintainers will reach out soon, and you can discuss the next steps with them.\n\nPlease include relevant GitHub Issues in the PR message when opening a Pull Request.\n\n## Development\n\nDeveloping `breakpoint` requires `nix` and optionally `docker`. We use `nix` to ensure reproducible development flow: it guarantees the identical versions of dependencies and tools. While `docker` is required only if you plan to build the Docker image of the Rendezvous server.\n\nFollow the instructions to install them for your operating system:\n\n- [Install nix](https://github.com/DeterminateSystems/nix-installer)\n- Docker: [Docker engine](https://docs.docker.com/engine/install/) or [OrbStack](https://docs.docker.com/engine/install/)\n\nWhen `nix` is installed, you can:\n\n- Run `nix develop` to enter a shell with every dependency pre-setup (e.g. Go, `buf`, etc.)\n- Use the \"nix environment selector\" VSCode extension to apply a nix environment in VSCode.\n\n### Building\n\nCompiling the Go binaries:\n\n```bash\n$ go build -o . ./cmd/...\n\n# Binaries available in the current working directory\n\n$ ls breakpoint; ls rendezvous;\n```\n\nInstalling the Go binaries:\n\n```bash\n$ go install ./cmd/...\n\n# Binaries installed in $GOPATH\n\n$ which breakpoint; which rendezvous;\n```\n\nBuilding the Docker image of Rendezvous server:\n\n```bash\n$ docker build . -t rendezvous:latest\n```\n\n### Protos\n\nBreakpoint uses gRPC and protos to implement both internal and public API. Internal API is used between the `breakpoint wait` process and the rest of CLI commands. The public API is provided by the `rendezvous` server to accept incoming `breakpoint` registrations.\n\nWhenever you change the protos definition under the [`api/`](../api) folder, then you must also regenerate the Go code:\n\n```bash\n$ buf generate\n```\n\nThis will add changes to the Go files under the [`api/`](../api) folder. Include them in your commit.\n"
  },
  {
    "path": "docs/server-setup.md",
    "content": "# Rendezvous Server Setup\n\nThe `rendezvous` source code is 100% open-source and you can self-host it wherever you want.\n\n## Requirements\n\nRendezvous Server needs two main properties in order to function:\n\n1. Public IP\n2. The process can listen to any port\n3. Traffic to all ports is allowed in both directions (ingress and egress)\n\n## Fly.io Deployment\n\nBreakpoint provides a ready-to-deploy Fly.io configuration.\n\nCreate a Fly.io application.\n\n```bash\n$ flyctl apps create rendezvous\n```\n\nAllocate a public IPv4 address and assign it to the application. Note that this is a paid feature of Fly.io.\n\n```bash\n$ flyctl ips allocate-v4 -a rendezvous\n```\n\nTake note of the public IPv4 address created before and deploy the `rendezvous` service.\n\n```bash\n$ flyctl deploy -a rendezvous --env PROXY_PUBLIC={public_ip}\n```\n\nDone! Now your instance of Rendezvous Server is listening to `{public_ip}:5000` endpoint.\n"
  },
  {
    "path": "examples/wait.withslack.json",
    "content": "{\n  \"webhooks\": [\n    {\n      \"url\": \"${SLACK_WEBHOOK_URL}\",\n      \"payload\": {\n        \"blocks\": [\n          {\n            \"type\": \"header\",\n            \"text\": {\n              \"type\": \"plain_text\",\n              \"text\": \"Workflow failed\",\n              \"emoji\": true\n            }\n          },\n          {\n            \"type\": \"section\",\n            \"text\": {\n              \"type\": \"mrkdwn\",\n              \"text\": \"*Repository:* <https://github.com/${GITHUB_REPOSITORY}/tree/${GITHUB_REF_NAME}|${GITHUB_REPOSITORY}> (${GITHUB_REF_NAME})\"\n            }\n          },\n          {\n            \"type\": \"section\",\n            \"text\": {\n              \"type\": \"mrkdwn\",\n              \"text\": \"*Workflow:* ${GITHUB_WORKFLOW} (<https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}|Run #${GITHUB_RUN_NUMBER}>)\"\n            }\n          },\n          {\n            \"type\": \"section\",\n            \"text\": {\n              \"type\": \"mrkdwn\",\n              \"text\": \"*SSH:* `ssh -p ${BREAKPOINT_PORT} runner@${BREAKPOINT_HOST}`\"\n            }\n          },\n          {\n            \"type\": \"section\",\n            \"text\": {\n              \"type\": \"mrkdwn\",\n              \"text\": \"*Expires:* in ${BREAKPOINT_TIME_LEFT} (${BREAKPOINT_EXPIRATION})\"\n            }\n          },\n          {\n            \"type\": \"context\",\n            \"elements\": [\n              {\n                \"type\": \"plain_text\",\n                \"text\": \"Actor: ${GITHUB_ACTOR}\",\n                \"emoji\": true\n              }\n            ]\n          }\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  outputs = {\n    self,\n    nixpkgs,\n    flake-utils,\n    ...\n  }:\n    flake-utils.lib.eachDefaultSystem (system: let\n      pkgs = nixpkgs.legacyPackages.${system};\n    in {\n      devShell = pkgs.mkShell {\n        buildInputs = with pkgs;\n          [\n            go_1_20\n            buf\n            protobuf\n            protoc-gen-go\n            protoc-gen-go-grpc\n            goreleaser\n          ];\n      };\n    });\n}\n\n"
  },
  {
    "path": "fly.toml",
    "content": "[build]\ndockerfile = \"Dockerfile\"\n\n[env]\nPROXY_LISTEN = \"fly-global-services:5000\"\nPROXY_PUBLIC = \"rendezvous.namespace.so\"\nPROXY_FRONTEND = '{\"kind\": \"proxy_proto\", \"port_start\": 2000, \"port_end\": 60000, \"listen_port\": 10000}'\nPROXY_VALIDATE_GITHUB_OIDC = \"true\"\n\n\n[[services]]\ninternal_port = 5000\nprotocol = \"udp\"\nauto_stop_machines = false\nauto_start_machines = false\n\n    [[services.ports]]\n    port = \"5000\"\n\n[[services]]\ninternal_port = 10000\nprotocol = \"tcp\"\nauto_stop_machines = false\nauto_start_machines = false\n\n    [[services.ports]]\n    handlers = [\"proxy_proto\"]\n    start_port = 2000\n    end_port = 60000\n\n[[services]]\ninternal_port = 10020\nprotocol = \"tcp\"\nauto_stop_machines = false\nauto_start_machines = false\n\n    [[services.ports]]\n    handlers = [\"http\"]\n    port = 80\n    force_https = true\n\n    [[services.ports]]\n    handlers = [\"tls\", \"http\"]\n    port = 443\n"
  },
  {
    "path": "go.mod",
    "content": "module namespacelabs.dev/breakpoint\n\ngo 1.20\n\nrequire (\n\tgithub.com/MicahParks/keyfunc v1.9.0\n\tgithub.com/creack/pty v1.1.18\n\tgithub.com/dustin/go-humanize v1.0.1\n\tgithub.com/gliderlabs/ssh v0.3.5\n\tgithub.com/golang-jwt/jwt/v4 v4.4.2\n\tgithub.com/google/go-cmp v0.5.9\n\tgithub.com/google/go-github/v52 v52.0.0\n\tgithub.com/grpc-ecosystem/go-grpc-middleware v1.4.0\n\tgithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0\n\tgithub.com/muesli/reflow v0.3.0\n\tgithub.com/pires/go-proxyproto v0.7.0\n\tgithub.com/pkg/sftp v1.13.5\n\tgithub.com/quic-go/quic-go v0.40.0\n\tgithub.com/rs/zerolog v1.29.1\n\tgithub.com/slack-go/slack v0.12.2\n\tgithub.com/spf13/cobra v1.7.0\n\tgo.uber.org/atomic v1.7.0\n\tgolang.org/x/crypto v0.7.0\n\tgolang.org/x/exp v0.0.0-20221205204356-47842c84f3db\n\tgolang.org/x/sync v0.2.0\n\tgoogle.golang.org/grpc v1.55.0\n\tgoogle.golang.org/protobuf v1.30.0\n\tinet.af/tcpproxy v0.0.0-20221017015627-91f861402626\n)\n\nrequire (\n\tgithub.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect\n\tgithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.2.0 // indirect\n\tgithub.com/cloudflare/circl v1.3.3 // indirect\n\tgithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect\n\tgithub.com/golang/protobuf v1.5.3 // indirect\n\tgithub.com/google/go-querystring v1.1.0 // indirect\n\tgithub.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect\n\tgithub.com/gorilla/websocket v1.4.2 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/kr/fs v0.1.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.12 // indirect\n\tgithub.com/mattn/go-isatty v0.0.14 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.12 // indirect\n\tgithub.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect\n\tgithub.com/onsi/ginkgo/v2 v2.9.5 // indirect\n\tgithub.com/prometheus/client_golang v1.15.1 // indirect\n\tgithub.com/prometheus/client_model v0.3.0 // indirect\n\tgithub.com/prometheus/common v0.42.0 // indirect\n\tgithub.com/prometheus/procfs v0.9.0 // indirect\n\tgithub.com/quic-go/qtls-go1-20 v0.4.1 // indirect\n\tgithub.com/rivo/uniseg v0.2.0 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgo.uber.org/mock v0.3.0 // indirect\n\tgolang.org/x/mod v0.11.0 // indirect\n\tgolang.org/x/net v0.10.0 // indirect\n\tgolang.org/x/oauth2 v0.7.0 // indirect\n\tgolang.org/x/sys v0.8.0 // indirect\n\tgolang.org/x/text v0.9.0 // indirect\n\tgolang.org/x/tools v0.9.1 // indirect\n\tgoogle.golang.org/appengine v1.6.7 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=\ngithub.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=\ngithub.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=\ngithub.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=\ngithub.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU=\ngithub.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=\ngithub.com/cespare/xxhash/v2 v2.2.0/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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=\ngithub.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=\ngithub.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=\ngithub.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\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/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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=\ngithub.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=\ngithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=\ngithub.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=\ngithub.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=\ngithub.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\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.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=\ngithub.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.5.2/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.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=\ngithub.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-github/v52 v52.0.0 h1:uyGWOY+jMQ8GVGSX8dkSwCzlehU3WfdxQ7GweO/JP7M=\ngithub.com/google/go-github/v52 v52.0.0/go.mod h1:WJV6VEEUPuMo5pXqqa2ZCZEdbQqua4zAk2MZTIo+m+4=\ngithub.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=\ngithub.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=\ngithub.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=\ngithub.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=\ngithub.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\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/konsorten/go-windows-terminal-sequences v1.0.1/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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\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/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=\ngithub.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=\ngithub.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=\ngithub.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=\ngithub.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=\ngithub.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=\ngithub.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=\ngithub.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=\ngithub.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=\ngithub.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=\ngithub.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=\ngithub.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=\ngithub.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=\ngithub.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=\ngithub.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=\ngithub.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=\ngithub.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=\ngithub.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=\ngithub.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=\ngithub.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw=\ngithub.com/quic-go/quic-go v0.40.0/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=\ngithub.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=\ngithub.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=\ngithub.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ=\ngithub.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=\ngithub.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=\ngithub.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\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/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.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.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=\ngo.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=\ngo.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=\ngolang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=\ngolang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=\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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\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.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=\ngolang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\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-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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-20201021035429-f5854403a974/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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=\ngolang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=\ngolang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=\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-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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=\ngolang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-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-20191204072324-ce4227a45e2e/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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\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-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\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.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=\ngolang.org/x/text v0.3.0/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.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.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\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-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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20191108193012-7d206e10da11/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-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=\ngolang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=\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=\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.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=\ngoogle.golang.org/appengine v1.6.7/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-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=\ngoogle.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\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.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=\ngoogle.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=\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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=\ngoogle.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\ninet.af/tcpproxy v0.0.0-20221017015627-91f861402626 h1:2dMP3Ox/Wh5BiItwOt4jxRsfzkgyBrHzx2nW28Yg6nc=\ninet.af/tcpproxy v0.0.0-20221017015627-91f861402626/go.mod h1:Tojt5kmHpDIR2jMojxzZK2w2ZR7OILODmUo2gaSwjrk=\n"
  },
  {
    "path": "pkg/README.md",
    "content": "Main components and packages."
  },
  {
    "path": "pkg/bcontrol/client.go",
    "content": "package bcontrol\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n\tpb \"namespacelabs.dev/breakpoint/api/private/v1\"\n\t\"namespacelabs.dev/breakpoint/pkg/bgrpc\"\n)\n\nfunc SocketPath() (string, error) {\n\tdir, err := os.UserConfigDir()\n\tif err != nil {\n\t\treturn dir, err\n\t}\n\n\treturn filepath.Join(dir, \"breakpoint/breakpoint.sock\"), nil\n}\n\nfunc Connect(ctx context.Context) (pb.ControlServiceClient, *grpc.ClientConn, error) {\n\tsocketPath, err := SocketPath()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tconn, err := bgrpc.DialContext(ctx, socketPath,\n\t\tgrpc.WithTransportCredentials(insecure.NewCredentials()),\n\t\tgrpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {\n\t\t\tvar d net.Dialer\n\t\t\treturn d.DialContext(ctx, \"unix\", socketPath)\n\t\t}))\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\treturn pb.NewControlServiceClient(conn), conn, nil\n}\n"
  },
  {
    "path": "pkg/bgrpc/bgrpc.go",
    "content": "package bgrpc\n\nimport (\n\t\"context\"\n\n\tgrpc_middleware \"github.com/grpc-ecosystem/go-grpc-middleware\"\n\tgrpc_prometheus \"github.com/grpc-ecosystem/go-grpc-prometheus\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc DialContext(ctx context.Context, target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {\n\tunary, streaming := clientInterceptors()\n\n\topts = append(opts,\n\t\tgrpc.WithStreamInterceptor(grpc_middleware.ChainStreamClient(streaming...)),\n\t\tgrpc.WithUnaryInterceptor(grpc_middleware.ChainUnaryClient(unary...)),\n\t)\n\n\treturn grpc.DialContext(ctx, target, opts...)\n}\n\nfunc clientInterceptors() ([]grpc.UnaryClientInterceptor, []grpc.StreamClientInterceptor) {\n\treturn []grpc.UnaryClientInterceptor{\n\t\t\tgrpc_prometheus.UnaryClientInterceptor,\n\t\t}, []grpc.StreamClientInterceptor{\n\t\t\tgrpc_prometheus.StreamClientInterceptor,\n\t\t}\n}\n"
  },
  {
    "path": "pkg/blog/blog.go",
    "content": "package blog\n\nimport (\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n)\n\nfunc init() {\n\tzerolog.TimeFieldFormat = time.RFC3339Nano\n}\n\nfunc New() zerolog.Logger {\n\treturn zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339Nano}).\n\t\tWith().Timestamp().Logger().Level(zerolog.InfoLevel)\n}\n"
  },
  {
    "path": "pkg/config/config.go",
    "content": "package config\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n\t\"google.golang.org/grpc/metadata\"\n\tinternalv1 \"namespacelabs.dev/breakpoint/api/private/v1\"\n\tv1 \"namespacelabs.dev/breakpoint/api/public/v1\"\n\t\"namespacelabs.dev/breakpoint/pkg/github\"\n\t\"namespacelabs.dev/breakpoint/pkg/githuboidc\"\n\t\"namespacelabs.dev/breakpoint/pkg/jsonfile\"\n)\n\nfunc LoadConfig(ctx context.Context, file string) (ParsedConfig, error) {\n\tvar cfg ParsedConfig\n\tif err := jsonfile.Load(file, &cfg.WaitConfig); err != nil {\n\t\treturn cfg, err\n\t}\n\n\tif cfg.Endpoint == \"\" {\n\t\treturn cfg, errors.New(\"missing endpoint\")\n\t}\n\n\tfor _, wh := range cfg.Webhooks {\n\t\tif wh.URL == \"\" {\n\t\t\treturn cfg, errors.New(\"webhook is missing url\")\n\t\t}\n\t}\n\n\tif len(cfg.Shell) == 0 {\n\t\tif sh, ok := os.LookupEnv(\"SHELL\"); ok {\n\t\t\tcfg.Shell = []string{sh}\n\t\t} else {\n\t\t\tif runtime.GOOS == \"windows\" {\n\t\t\t\tcfg.Shell = []string{\"C:\\\\Windows\\\\System32\\\\cmd.exe\"}\n\t\t\t} else {\n\t\t\t\tcfg.Shell = []string{\"/bin/sh\"}\n\t\t\t}\n\t\t}\n\t}\n\n\trequireGitHubOIDC := false\n\tfor _, feature := range cfg.Enable {\n\t\tswitch feature {\n\t\tcase \"github/oidc\":\n\t\t\t// Force enable.\n\t\t\trequireGitHubOIDC = false\n\n\t\tdefault:\n\t\t\treturn cfg, fmt.Errorf(\"unknown feature %q\", feature)\n\t\t}\n\t}\n\n\tcfg.RegisterMetadata = metadata.MD{}\n\tif githuboidc.OIDCAvailable() || requireGitHubOIDC {\n\t\ttoken, err := githuboidc.JWT(ctx, v1.GitHubOIDCAudience)\n\t\tif err != nil {\n\t\t\tif requireGitHubOIDC {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\n\t\t\tzerolog.Ctx(ctx).Warn().Err(err).Msg(\"Failed to obtain GitHUB OIDC token\")\n\t\t} else {\n\t\t\tcfg.RegisterMetadata[v1.GitHubOIDCTokenHeader] = []string{token.Value}\n\t\t}\n\t}\n\n\tdur, err := time.ParseDuration(cfg.Duration)\n\tif err != nil {\n\t\treturn cfg, err\n\t}\n\n\tcfg.ParsedDuration = dur\n\n\tkeyMap, err := github.ResolveSSHKeys(ctx, cfg.AuthorizedGithubUsers)\n\tif err != nil {\n\t\treturn cfg, err\n\t}\n\n\trevIndex := map[string]string{}\n\n\tfor _, key := range cfg.AuthorizedKeys {\n\t\trevIndex[key] = key\n\t}\n\n\tfor user, keys := range keyMap {\n\t\tfor _, key := range keys {\n\t\t\trevIndex[key] = user\n\t\t}\n\t}\n\n\tcfg.AllKeys = revIndex\n\treturn cfg, nil\n}\n\ntype ParsedConfig struct {\n\tinternalv1.WaitConfig\n\n\tAllKeys          map[string]string // Key ID -> Owned name\n\tParsedDuration   time.Duration\n\tRegisterMetadata metadata.MD\n}\n"
  },
  {
    "path": "pkg/execbackground/bg_unix.go",
    "content": "//go:build !windows\n\npackage execbackground\n\nimport (\n\t\"os/exec\"\n\t\"syscall\"\n)\n\nfunc SetCreateSession(cmd *exec.Cmd) {\n\tcmd.SysProcAttr = &syscall.SysProcAttr{\n\t\tSetsid: true,\n\t}\n}\n"
  },
  {
    "path": "pkg/execbackground/bg_windows.go",
    "content": "//go:build windows\n\npackage execbackground\n\nimport \"os/exec\"\n\nfunc SetCreateSession(cmd *exec.Cmd) {\n\tpanic(\"not supported\")\n}\n"
  },
  {
    "path": "pkg/github/sshkeys.go",
    "content": "package github\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n)\n\nfunc ResolveSSHKeys(ctx context.Context, usernames []string) (map[string][]string, error) {\n\t// Fetch in sequence to minimize how many requests in parallel we issue to GitHub.\n\n\tm := map[string][]string{}\n\tfor _, username := range usernames {\n\t\tt := time.Now()\n\n\t\tkeys, err := fetchKeys(username)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to fetch SSH keys for GitHub user %q: %w\", username, err)\n\t\t}\n\n\t\tif len(keys) == 0 {\n\t\t\tzerolog.Ctx(ctx).Warn().Str(\"username\", username).Dur(\"took\", time.Since(t)).Msg(\"No keys found\")\n\t\t\tcontinue\n\t\t}\n\n\t\tm[username] = keys\n\n\t\tzerolog.Ctx(ctx).Info().Str(\"username\", username).Dur(\"took\", time.Since(t)).Msg(\"Resolved keys\")\n\t}\n\n\treturn m, nil\n}\n\nfunc fetchKeys(username string) ([]string, error) {\n\tresp, err := http.Get(fmt.Sprintf(\"https://github.com/%s.keys\", username))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif resp.StatusCode != 200 {\n\t\treturn nil, fmt.Errorf(\"unexpected status code %d\", resp.StatusCode)\n\t}\n\n\tcontents, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read body: %w\", err)\n\t}\n\n\tvar keys []string\n\tfor _, line := range strings.FieldsFunc(strings.TrimSpace(string(contents)), func(r rune) bool { return r == '\\n' }) {\n\t\tkeys = append(keys, strings.TrimSpace(line))\n\t}\n\n\treturn keys, nil\n}\n"
  },
  {
    "path": "pkg/githuboidc/claims.go",
    "content": "package githuboidc\n\nimport \"github.com/golang-jwt/jwt/v4\"\n\ntype Claims struct {\n\tjwt.RegisteredClaims\n\n\tJobWorkflowRef    string `json:\"job_workflow_ref\"`\n\tSha               string `json:\"sha\"`\n\tEventName         string `json:\"event_name\"`\n\tRepository        string `json:\"repository\"`\n\tWorkflow          string `json:\"workflow\"`\n\tRef               string `json:\"ref\"`\n\tJobWorkflowSha    string `json:\"job_workflow_sha\"`\n\tRunnerEnvironment string `json:\"runner_environment\"`\n\tRepositoryID      string `json:\"repository_id\"`\n\tRepositoryOwner   string `json:\"repository_owner\"`\n\tRepositoryOwnerID string `json:\"repository_owner_id\"`\n\tWorkflowRef       string `json:\"workflow_ref\"`\n\tWorkflowSha       string `json:\"workflow_sha\"`\n\tRunID             string `json:\"run_id\"`\n\tRunAttempt        string `json:\"run_attempt\"`\n}\n"
  },
  {
    "path": "pkg/githuboidc/gh.go",
    "content": "package githuboidc\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\n\t\"namespacelabs.dev/breakpoint/pkg/httperrors\"\n)\n\nvar ErrMissingIdTokenWrite = errors.New(\"please add `id-token: write` to your workflow permissions\")\n\nconst (\n\tuserAgent = \"actions/oidc-client\"\n)\n\ntype Token struct {\n\tValue string `json:\"value\"`\n}\n\nfunc OIDCAvailable() bool {\n\tx, y := oidcConf()\n\treturn x != \"\" && y != \"\"\n}\n\nfunc JWT(ctx context.Context, audience string) (*Token, error) {\n\tidTokenURL, idToken := oidcConf()\n\tif idTokenURL == \"\" || idToken == \"\" {\n\t\treturn nil, ErrMissingIdTokenWrite\n\t}\n\n\tif audience != \"\" {\n\t\tidTokenURL += fmt.Sprintf(\"&audience=%s\", url.QueryEscape(audience))\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", idTokenURL, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"github/oidc: failed to create HTTP request: %w\", err)\n\t}\n\n\treq.Header.Add(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.Header.Add(\"User-Agent\", userAgent)\n\treq.Header.Add(\"Authorization\", \"Bearer \"+idToken)\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"github/oidc: failed to request github JWT: %w\", err)\n\t}\n\n\tdefer resp.Body.Close()\n\n\tif err := httperrors.MaybeError(resp); err != nil {\n\t\treturn nil, fmt.Errorf(\"github/oidc: failed to obtain token: %v\", err)\n\t}\n\n\tvar token Token\n\tif err := json.NewDecoder(resp.Body).Decode(&token); err != nil {\n\t\treturn nil, fmt.Errorf(\"github/oidc: bad response: %w\", err)\n\t}\n\n\treturn &token, nil\n}\n\nfunc oidcConf() (string, string) {\n\tidTokenURL := os.Getenv(\"ACTIONS_ID_TOKEN_REQUEST_URL\")\n\tidToken := os.Getenv(\"ACTIONS_ID_TOKEN_REQUEST_TOKEN\")\n\n\treturn idTokenURL, idToken\n}\n"
  },
  {
    "path": "pkg/githuboidc/verifier.go",
    "content": "package githuboidc\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/MicahParks/keyfunc\"\n\t\"github.com/golang-jwt/jwt/v4\"\n\t\"github.com/rs/zerolog\"\n)\n\nconst (\n\tgithubJWKSURL = \"https://token.actions.githubusercontent.com/.well-known/jwks\"\n)\n\nfunc ProvideVerifier(ctx context.Context) (*keyfunc.JWKS, error) {\n\toptions := keyfunc.Options{\n\t\tCtx: ctx,\n\t\tRefreshErrorHandler: func(err error) {\n\t\t\tzerolog.Ctx(ctx).Err(err).Str(\"jwks_url\", githubJWKSURL).Msg(\"Failed to refresh JWKS\")\n\t\t},\n\t\tRefreshInterval:   time.Hour,\n\t\tRefreshRateLimit:  time.Minute * 5,\n\t\tRefreshTimeout:    time.Second * 10,\n\t\tRefreshUnknownKID: true,\n\t}\n\n\treturn keyfunc.Get(githubJWKSURL, options)\n}\n\nfunc Validate(ctx context.Context, jwks *keyfunc.JWKS, tokenStr string) (*Claims, error) {\n\tclaims := &Claims{}\n\n\ttoken, err := jwt.ParseWithClaims(tokenStr, claims, jwks.Keyfunc)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to verify Github JWT: %w\", err)\n\t}\n\n\tif !token.Valid {\n\t\treturn nil, errors.New(\"invalid Github JWT\")\n\t}\n\n\treturn claims, nil\n}\n"
  },
  {
    "path": "pkg/httperrors/httperrors.go",
    "content": "package httperrors\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n)\n\ntype HttpError struct {\n\tStatusCode  int\n\tServerError string\n}\n\nfunc (he HttpError) Error() string {\n\tif len(he.ServerError) > 0 {\n\t\treturn fmt.Sprintf(\"request failed with %s, got from the server:\\n%s\", http.StatusText(he.StatusCode), he.ServerError)\n\t}\n\n\treturn fmt.Sprintf(\"request failed with %s\", http.StatusText(he.StatusCode))\n}\n\nfunc MaybeError(resp *http.Response) error {\n\tif resp.StatusCode != http.StatusOK {\n\t\tbodyBytes, _ := io.ReadAll(resp.Body)\n\t\treturn &HttpError{StatusCode: resp.StatusCode, ServerError: string(bodyBytes)}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/internalserver/internalserver.go",
    "content": "package internalserver\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"golang.org/x/sync/errgroup\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\tpb \"namespacelabs.dev/breakpoint/api/private/v1\"\n\t\"namespacelabs.dev/breakpoint/pkg/bcontrol\"\n\t\"namespacelabs.dev/breakpoint/pkg/waiter\"\n)\n\ntype waiterService struct {\n\tmanager *waiter.Manager\n\tpb.UnimplementedControlServiceServer\n}\n\nfunc ListenAndServe(ctx context.Context, mgr *waiter.Manager) error {\n\tsocketPath, err := bcontrol.SocketPath()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := os.MkdirAll(filepath.Dir(socketPath), 0755); err != nil {\n\t\treturn err\n\t}\n\n\t_ = os.Remove(socketPath) // Remove any leftovers.\n\n\tdefer func() {\n\t\t_ = os.Remove(socketPath)\n\t}()\n\n\tvar d net.ListenConfig\n\tlis, err := d.Listen(ctx, \"unix\", socketPath)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to listen: %v\", err)\n\t}\n\n\tgrpcServer := grpc.NewServer()\n\tpb.RegisterControlServiceServer(grpcServer, waiterService{\n\t\tmanager: mgr,\n\t})\n\n\teg, ctx := errgroup.WithContext(ctx)\n\n\teg.Go(func() error {\n\t\t<-ctx.Done()\n\t\tgrpcServer.Stop()\n\t\treturn nil\n\t})\n\n\teg.Go(func() error {\n\t\treturn grpcServer.Serve(lis)\n\t})\n\n\treturn eg.Wait()\n}\n\nfunc (g waiterService) Extend(ctx context.Context, req *pb.ExtendRequest) (*pb.ExtendResponse, error) {\n\texpiration := g.manager.ExtendWait(req.WaitFor.AsDuration())\n\treturn &pb.ExtendResponse{\n\t\tExpiration: timestamppb.New(expiration),\n\t}, nil\n}\n\nfunc (g waiterService) Status(ctx context.Context, req *emptypb.Empty) (*pb.StatusResponse, error) {\n\tstatus := g.manager.Status()\n\treturn &pb.StatusResponse{\n\t\tExpiration:     timestamppb.New(status.Expiration),\n\t\tEndpoint:       status.Endpoint,\n\t\tNumConnections: status.NumConnections,\n\t}, nil\n}\n\nfunc (g waiterService) Resume(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) {\n\tg.manager.StopWait()\n\treturn &emptypb.Empty{}, nil\n}\n"
  },
  {
    "path": "pkg/jsonfile/load.go",
    "content": "package jsonfile\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n)\n\nfunc Load(filename string, target any) error {\n\tf, err := os.Open(filename)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn json.NewDecoder(f).Decode(target)\n}\n"
  },
  {
    "path": "pkg/passthrough/listener.go",
    "content": "package passthrough\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\n\t\"go.uber.org/atomic\"\n)\n\ntype Listener struct {\n\tctx    context.Context\n\taddr   net.Addr\n\tch     chan net.Conn\n\tclosed *atomic.Bool\n}\n\nfunc NewListener(ctx context.Context, addr net.Addr) Listener {\n\treturn Listener{ctx: ctx, addr: addr, ch: make(chan net.Conn), closed: atomic.NewBool(false)}\n}\n\nfunc (pl Listener) Accept() (net.Conn, error) {\n\tselect {\n\tcase <-pl.ctx.Done():\n\t\treturn nil, pl.ctx.Err()\n\n\tcase conn, ok := <-pl.ch:\n\t\tif !ok {\n\t\t\treturn nil, errors.New(\"listener is closed\")\n\t\t}\n\t\treturn conn, nil\n\t}\n}\n\nfunc (pl Listener) Addr() net.Addr {\n\treturn pl.addr\n}\n\nfunc (pl Listener) Close() error {\n\tif !pl.closed.Swap(true) {\n\t\tclose(pl.ch)\n\t\treturn nil\n\t} else {\n\t\treturn errors.New(\"already closed\")\n\t}\n}\n\nfunc (pl Listener) Offer(conn net.Conn) error {\n\tif pl.closed.Load() {\n\t\treturn errors.New(\"listener closed\")\n\t}\n\n\tpl.ch <- conn\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/quicgrpc/grpccreds.go",
    "content": "package quicgrpc\n\nimport (\n\t\"context\"\n\t\"net\"\n\n\t\"github.com/quic-go/quic-go\"\n\t\"google.golang.org/grpc/credentials\"\n\t\"namespacelabs.dev/breakpoint/pkg/quicnet\"\n)\n\ntype QuicCreds struct {\n\tNonQuicCreds credentials.TransportCredentials\n}\n\nvar _ credentials.TransportCredentials = QuicCreds{}\n\nfunc (m QuicCreds) ClientHandshake(ctx context.Context, addr string, conn net.Conn) (net.Conn, credentials.AuthInfo, error) {\n\treturn m.NonQuicCreds.ClientHandshake(ctx, addr, conn)\n}\n\nfunc (m QuicCreds) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) {\n\tif quic, ok := conn.(quicnet.Conn); ok {\n\t\treturn conn, QuicAuthInfo{Conn: quic.Conn}, nil\n\t}\n\n\treturn m.NonQuicCreds.ServerHandshake(conn)\n}\n\nfunc (m QuicCreds) Info() credentials.ProtocolInfo {\n\treturn credentials.ProtocolInfo{SecurityProtocol: \"insecure\"}\n}\n\nfunc (m QuicCreds) Clone() credentials.TransportCredentials {\n\treturn QuicCreds{NonQuicCreds: m.NonQuicCreds.Clone()}\n}\n\nfunc (m QuicCreds) OverrideServerName(string) error {\n\treturn nil\n}\n\ntype QuicAuthInfo struct {\n\tcredentials.CommonAuthInfo\n\tConn quic.Connection\n}\n\nfunc (QuicAuthInfo) AuthType() string {\n\treturn \"quic\"\n}\n"
  },
  {
    "path": "pkg/quicnet/conn.go",
    "content": "package quicnet\n\nimport (\n\t\"context\"\n\t\"net\"\n\n\t\"github.com/quic-go/quic-go\"\n)\n\ntype Conn struct {\n\tquic.Stream\n\tConn quic.Connection\n}\n\nfunc (cw Conn) LocalAddr() net.Addr {\n\treturn cw.Conn.LocalAddr()\n}\n\nfunc (cw Conn) RemoteAddr() net.Addr {\n\treturn cw.Conn.RemoteAddr()\n}\n\nfunc OpenStream(ctx context.Context, conn quic.Connection) (Conn, error) {\n\tstream, err := conn.OpenStreamSync(ctx)\n\tif err != nil {\n\t\treturn Conn{}, err\n\t}\n\n\treturn Conn{Stream: stream, Conn: conn}, nil\n\n}\n"
  },
  {
    "path": "pkg/quicnet/listener.go",
    "content": "package quicnet\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/quic-go/quic-go\"\n\t\"github.com/rs/zerolog\"\n)\n\nvar (\n\terrClosed        = errors.New(\"closed\")\n\terrAlreadyClosed = errors.New(\"already closed\")\n)\n\ntype Listener struct {\n\tctx      context.Context\n\tlistener quic.Listener\n\n\tmu    sync.Mutex\n\tcond  *sync.Cond\n\tinbox []net.Conn\n\tlErr  error // If set, the listener is closed.\n}\n\nfunc NewListener(ctx context.Context, l quic.Listener) *Listener {\n\tlst := &Listener{ctx: ctx, listener: l}\n\tlst.cond = sync.NewCond(&lst.mu)\n\tgo lst.loop()\n\treturn lst\n}\n\nfunc (l *Listener) loop() {\n\tfor {\n\t\tconn, err := l.listener.Accept(l.ctx)\n\t\tif err != nil {\n\t\t\t_ = l.closeWithErr(err)\n\t\t\treturn\n\t\t}\n\n\t\tgo l.waitForStream(conn)\n\t}\n}\n\nfunc (l *Listener) closeWithErr(err error) error {\n\tl.mu.Lock()\n\twasErr := l.lErr\n\tinbox := l.inbox\n\tif l.lErr == nil {\n\t\tl.lErr = err\n\t\tl.inbox = nil\n\t}\n\tl.cond.Broadcast()\n\tl.mu.Unlock()\n\n\tif wasErr != nil {\n\t\treturn errAlreadyClosed\n\t}\n\n\tfor _, conn := range inbox {\n\t\t_ = conn.Close()\n\t}\n\n\t_ = l.listener.Close()\n\n\treturn nil\n}\n\nfunc (l *Listener) waitForStream(conn quic.Connection) {\n\t// If we don't see a stream within the deadline, then close the connection.\n\tctx, done := context.WithTimeout(l.ctx, 10*time.Second)\n\tdefer done()\n\n\tstream, err := conn.AcceptStream(ctx)\n\tif err != nil {\n\t\tzerolog.Ctx(ctx).Info().Stringer(\"remote_addr\", conn.RemoteAddr()).\n\t\t\tStringer(\"local_addr\", conn.LocalAddr()).Err(err).Msg(\"Failed to accept stream\")\n\t\tconn.CloseWithError(0, \"\")\n\t\treturn\n\t}\n\n\tl.queue(conn, stream)\n}\n\nfunc (l *Listener) queue(conn quic.Connection, stream quic.Stream) {\n\tl.mu.Lock()\n\tlErr := l.lErr\n\tif l.lErr == nil {\n\t\tl.inbox = append(l.inbox, Conn{Conn: conn, Stream: stream})\n\t\tl.cond.Signal()\n\t}\n\tl.mu.Unlock()\n\n\tif lErr != nil {\n\t\tzerolog.Ctx(l.ctx).Info().Stringer(\"remote_addr\", conn.RemoteAddr()).\n\t\t\tStringer(\"local_addr\", conn.LocalAddr()).Err(lErr).Msg(\"Listener was closed\")\n\t\tconn.CloseWithError(0, \"\")\n\t}\n}\n\nfunc (l *Listener) Accept() (net.Conn, error) {\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\n\tfor len(l.inbox) == 0 {\n\t\tl.cond.Wait()\n\n\t\tif l.lErr != nil {\n\t\t\treturn nil, l.lErr\n\t\t}\n\n\t\tif err := l.ctx.Err(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tconn := l.inbox[0]\n\tl.inbox = l.inbox[1:]\n\treturn conn, nil\n}\n\nfunc (l *Listener) Close() error {\n\treturn l.closeWithErr(errClosed)\n}\n\nfunc (l *Listener) Addr() net.Addr {\n\treturn l.listener.Addr()\n}\n"
  },
  {
    "path": "pkg/quicproxy/proxyproto.go",
    "content": "package quicproxy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net\"\n\t\"sync\"\n\n\tproxyproto \"github.com/pires/go-proxyproto\"\n\t\"github.com/rs/zerolog\"\n)\n\ntype ProxyProtoFrontend struct {\n\tListenPort         int\n\tPortStart, PortEnd int\n\tPublicAddr         string\n\n\tmu    sync.RWMutex\n\talloc map[int]func(net.Conn)\n}\n\nfunc (pf *ProxyProtoFrontend) ListenAndServe(ctx context.Context) error {\n\tvar l net.ListenConfig\n\tlst, err := l.Listen(ctx, \"tcp\", fmt.Sprintf(\":%d\", pf.ListenPort))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tgo func() {\n\t\t<-ctx.Done()\n\t\t_ = lst.Close()\n\t}()\n\n\tproxyListener := &proxyproto.Listener{Listener: lst}\n\n\tfor {\n\t\tconn, err := proxyListener.Accept()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tl := zerolog.Ctx(ctx).With().Stringer(\"remote_addr\", conn.RemoteAddr()).\n\t\t\tStringer(\"local_addr\", conn.LocalAddr()).Logger()\n\n\t\tif tcpaddr, ok := conn.LocalAddr().(*net.TCPAddr); ok {\n\t\t\tgo func() {\n\t\t\t\tpf.mu.RLock()\n\t\t\t\thandler, ok := pf.alloc[tcpaddr.Port]\n\t\t\t\tif ok {\n\t\t\t\t\tl.Debug().Msg(\"New connection\")\n\t\t\t\t\t// Call handler with the rlock held to make sure we're\n\t\t\t\t\t// always handling streams consistently. Handler will\n\t\t\t\t\t// quickly spawn a go routine and return.\n\t\t\t\t\thandler(conn)\n\t\t\t\t} else {\n\t\t\t\t\tl.Debug().Msg(\"No match\")\n\t\t\t\t}\n\t\t\t\tpf.mu.RUnlock()\n\n\t\t\t\t// Close without holding the lock.\n\t\t\t\tif !ok {\n\t\t\t\t\t_ = conn.Close()\n\t\t\t\t}\n\t\t\t}()\n\t\t} else {\n\t\t\tl.Debug().Msg(\"Ignored non-tcp\")\n\t\t\t_ = conn.Close()\n\t\t}\n\t}\n}\n\nfunc (pf *ProxyProtoFrontend) allocate(ctx context.Context, handler func(net.Conn)) (int, func(), error) {\n\tpf.mu.Lock()\n\tdefer pf.mu.Unlock()\n\n\t// XXX naive; move to pre-shuffle.\n\tfor i := 0; i < 100; i++ {\n\t\tport := pf.PortStart + rand.Int()%(pf.PortEnd-pf.PortStart)\n\t\tif _, ok := pf.alloc[port]; !ok {\n\t\t\tif pf.alloc == nil {\n\t\t\t\tpf.alloc = map[int]func(net.Conn){}\n\t\t\t}\n\t\t\tpf.alloc[port] = handler\n\t\t\treturn port, func() {\n\t\t\t\tpf.mu.Lock()\n\t\t\t\tdelete(pf.alloc, port)\n\t\t\t\tpf.mu.Unlock()\n\t\t\t}, nil\n\t\t}\n\t}\n\n\treturn -1, nil, errors.New(\"failed to allocate port\")\n}\n\nfunc (pf *ProxyProtoFrontend) Handle(ctx context.Context, handlers Handlers) error {\n\tport, cleanup, err := pf.allocate(ctx, func(conn net.Conn) {\n\t\tgo handlers.HandleConn(conn)\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer cleanup()\n\n\talloc := Allocation{Endpoint: fmt.Sprintf(\"%s:%d\", pf.PublicAddr, port)}\n\n\tif err := handlers.OnAllocation(alloc); err != nil {\n\t\treturn err\n\t}\n\n\t<-ctx.Done()\n\tctxErr := ctx.Err()\n\n\tif handlers.OnCleanup != nil {\n\t\thandlers.OnCleanup(alloc, ctxErr)\n\t}\n\n\treturn ctxErr\n}\n"
  },
  {
    "path": "pkg/quicproxy/rawproto.go",
    "content": "package quicproxy\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\n\t\"github.com/rs/zerolog\"\n)\n\ntype RawFrontend struct {\n\tPublicAddr string\n}\n\nfunc (rf RawFrontend) ListenAndServe(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (rf RawFrontend) Handle(ctx context.Context, handlers Handlers) error {\n\tvar d net.ListenConfig\n\tlistener, err := d.Listen(ctx, \"tcp\", \"0.0.0.0:0\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// If the context is canceled (e.g. the registration stream breaks), also\n\t// stop the listener.\n\tgo func() {\n\t\t<-ctx.Done()\n\t\t_ = listener.Close()\n\t}()\n\n\t// If we leave the Serve handler for reasons other than the listener\n\t// closing, make sure it's closed.\n\tdefer func() {\n\t\t_ = listener.Close()\n\t}()\n\n\tport := listener.Addr().(*net.TCPAddr).Port\n\talloc := Allocation{Endpoint: fmt.Sprintf(\"%s:%d\", rf.PublicAddr, port)}\n\n\tif err := handlers.OnAllocation(alloc); err != nil {\n\t\treturn err\n\t}\n\n\tfor {\n\t\tconn, err := listener.Accept()\n\t\tif err != nil {\n\t\t\tif handlers.OnCleanup != nil {\n\t\t\t\thandlers.OnCleanup(alloc, err)\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\tzerolog.Ctx(ctx).Debug().Stringer(\"remote_addr\", conn.RemoteAddr()).\n\t\t\tStringer(\"local_addr\", conn.LocalAddr()).\n\t\t\tStr(\"allocation\", alloc.Endpoint).Msg(\"New connection\")\n\n\t\tgo handlers.HandleConn(conn)\n\t}\n}\n"
  },
  {
    "path": "pkg/quicproxy/serve.go",
    "content": "package quicproxy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/quic-go/quic-go\"\n\t\"github.com/rs/zerolog\"\n\t\"inet.af/tcpproxy\"\n\t\"namespacelabs.dev/breakpoint/pkg/quicnet\"\n)\n\ntype Allocation struct {\n\tEndpoint string\n}\n\ntype ProxyFrontend interface {\n\tListenAndServe(context.Context) error\n\tHandle(context.Context, Handlers) error\n}\n\ntype Handlers struct {\n\tOnAllocation func(Allocation) error\n\tOnCleanup    func(Allocation, error)\n\tHandleConn   func(net.Conn)\n}\n\nfunc ServeProxy(ctx context.Context, frontend ProxyFrontend, conn quic.Connection, callback func(Allocation) error) error {\n\tbackend := tcpproxy.To(\"backend\")\n\tbackend.DialTimeout = 30 * time.Second\n\tbackend.ProxyProtocolVersion = 1\n\tbackend.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {\n\t\treturn quicnet.OpenStream(ctx, conn)\n\t}\n\n\treturn frontend.Handle(ctx, Handlers{\n\t\tOnAllocation: func(alloc Allocation) error {\n\t\t\tzerolog.Ctx(ctx).Info().Str(\"allocation\", alloc.Endpoint).Msg(\"New allocation\")\n\t\t\treturn callback(alloc)\n\t\t},\n\t\tOnCleanup: func(alloc Allocation, err error) {\n\t\t\tzerolog.Ctx(ctx).Info().Str(\"allocation\", alloc.Endpoint).Err(cancelIsOK(err)).Msg(\"Released allocation\")\n\t\t},\n\t\tHandleConn: backend.HandleConn,\n\t})\n}\n\nfunc cancelIsOK(err error) error {\n\tif errors.Is(err, context.Canceled) {\n\t\treturn nil\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "pkg/quicproxy/service.go",
    "content": "package quicproxy\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/MicahParks/keyfunc\"\n\t\"github.com/quic-go/quic-go\"\n\t\"github.com/rs/zerolog\"\n\t\"golang.org/x/exp/slices\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n\t\"google.golang.org/grpc/metadata\"\n\t\"google.golang.org/grpc/peer\"\n\t\"google.golang.org/grpc/status\"\n\tapipb \"namespacelabs.dev/breakpoint/api/public/v1\"\n\t\"namespacelabs.dev/breakpoint/pkg/githuboidc\"\n\t\"namespacelabs.dev/breakpoint/pkg/quicgrpc\"\n\t\"namespacelabs.dev/breakpoint/pkg/quicnet\"\n\t\"namespacelabs.dev/breakpoint/pkg/quicproxyclient\"\n\t\"namespacelabs.dev/breakpoint/pkg/tlscerts\"\n)\n\ntype Server struct {\n\tp        ProxyFrontend\n\tlistener quic.Listener\n\tghJWKS   *keyfunc.JWKS\n}\n\ntype ServerOpts struct {\n\tProxyFrontend    ProxyFrontend\n\tListenAddr       string\n\tSubjects         tlscerts.Subjects\n\tEnableGitHubOIDC bool\n}\n\nfunc NewServer(ctx context.Context, opts ServerOpts) (*Server, error) {\n\tt := time.Now()\n\tpublic, private, err := tlscerts.GenerateECDSAPair(opts.Subjects, 365*24*time.Hour)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tzerolog.Ctx(ctx).Info().Dur(\"took\", time.Since(t)).Msg(\"Generated new keys\")\n\n\tsrv := &Server{p: opts.ProxyFrontend}\n\n\tif opts.EnableGitHubOIDC {\n\t\tt = time.Now()\n\t\tjwks, err := githuboidc.ProvideVerifier(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tzerolog.Ctx(ctx).Info().Dur(\"took\", time.Since(t)).Msg(\"Prepared GitHub JWKS\")\n\t\tsrv.ghJWKS = jwks\n\t}\n\n\tcert, err := tls.X509KeyPair(public, private)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttlsconf := &tls.Config{\n\t\tCertificates: []tls.Certificate{cert},\n\t\tNextProtos:   []string{apipb.QuicProto},\n\t}\n\n\tlistener, err := quic.ListenAddr(opts.ListenAddr, tlsconf, quicproxyclient.DefaultConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsrv.listener = *listener\n\treturn srv, nil\n}\n\nfunc (srv *Server) Close() error {\n\treturn srv.listener.Close()\n}\n\nfunc (srv *Server) Serve(ctx context.Context) error {\n\tzerolog.Ctx(ctx).Info().Str(\"addr\", srv.listener.Addr().String()).Msg(\"Listening\")\n\n\tgrpcServer := grpc.NewServer(grpc.Creds(quicgrpc.QuicCreds{NonQuicCreds: insecure.NewCredentials()}))\n\tapipb.RegisterProxyServiceServer(grpcServer, server{\n\t\tlogger:   zerolog.Ctx(ctx).With().Logger(),\n\t\tfrontend: srv.p,\n\t\tghJWKS:   srv.ghJWKS,\n\t})\n\treturn grpcServer.Serve(quicnet.NewListener(ctx, srv.listener))\n}\n\ntype server struct {\n\tapipb.UnimplementedProxyServiceServer\n\n\tlogger   zerolog.Logger\n\tfrontend ProxyFrontend\n\tghJWKS   *keyfunc.JWKS\n\n\trestrictToRepositories []string\n\trestrictToOwners       []string\n}\n\nfunc (srv server) Register(req *apipb.RegisterRequest, server apipb.ProxyService_RegisterServer) error {\n\tpeer, _ := peer.FromContext(server.Context())\n\tquic, ok := peer.AuthInfo.(quicgrpc.QuicAuthInfo)\n\tif !ok {\n\t\treturn errors.New(\"internal error, expected quic\")\n\t}\n\n\tgithubClaims, logger := validateGitHubOIDC(server.Context(), srv.logger, srv.ghJWKS)\n\n\tif len(srv.restrictToRepositories) > 0 {\n\t\tif githubClaims == nil || !slices.Contains(srv.restrictToRepositories, githubClaims.Repository) {\n\t\t\treturn status.Errorf(codes.PermissionDenied, \"repository %q not allowed\", githubClaims.Repository)\n\t\t}\n\t}\n\n\tif len(srv.restrictToOwners) > 0 {\n\t\tif githubClaims == nil || !slices.Contains(srv.restrictToOwners, githubClaims.RepositoryOwner) {\n\t\t\treturn status.Errorf(codes.PermissionDenied, \"repository owner %q not allowed\", githubClaims.RepositoryOwner)\n\t\t}\n\t}\n\n\treturn ServeProxy(logger.WithContext(server.Context()), srv.frontend, quic.Conn, func(alloc Allocation) error {\n\t\treturn server.Send(&apipb.RegisterResponse{Endpoint: alloc.Endpoint})\n\t})\n}\n\nfunc validateGitHubOIDC(ctx context.Context, logger zerolog.Logger, jwks *keyfunc.JWKS) (*githuboidc.Claims, zerolog.Logger) {\n\tif jwks != nil {\n\t\tmd, _ := metadata.FromIncomingContext(ctx)\n\t\tif token, ok := md[apipb.GitHubOIDCTokenHeader]; ok && len(token) > 0 {\n\t\t\tclaims, err := githuboidc.Validate(ctx, jwks, token[0])\n\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warn().Err(err).Msg(\"Failed to validate GitHub OIDC Token\")\n\t\t\t} else if slices.Contains(claims.Audience, apipb.GitHubOIDCAudience) {\n\t\t\t\tlogger.Warn().Str(\"expected\", apipb.GitHubOIDCAudience).Strs(\"audience\", claims.Audience).\n\t\t\t\t\tMsg(\"Failed to validate GitHub OIDC Token audience\")\n\t\t\t} else {\n\t\t\t\treturn claims, logger.With().Str(\"repository\", claims.Repository).Logger()\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, logger\n}\n"
  },
  {
    "path": "pkg/quicproxyclient/client.go",
    "content": "package quicproxyclient\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"net\"\n\t\"time\"\n\n\tproxyproto \"github.com/pires/go-proxyproto\"\n\t\"github.com/quic-go/quic-go\"\n\t\"github.com/rs/zerolog\"\n\t\"golang.org/x/sync/errgroup\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n\t\"google.golang.org/grpc/metadata\"\n\tv1 \"namespacelabs.dev/breakpoint/api/public/v1\"\n\t\"namespacelabs.dev/breakpoint/pkg/bgrpc\"\n\t\"namespacelabs.dev/breakpoint/pkg/quicnet\"\n)\n\nvar DefaultConfig = &quic.Config{\n\tMaxIdleTimeout:  5 * time.Second,\n\tKeepAlivePeriod: 30 * time.Second,\n}\n\ntype Handlers struct {\n\tOnAllocation func(string)\n\tProxy        func(net.Conn) error\n}\n\nfunc Serve(ctx context.Context, endpoint string, md metadata.MD, handlers Handlers) error {\n\ttlsConf := &tls.Config{\n\t\tInsecureSkipVerify: true,\n\t\tNextProtos:         []string{v1.QuicProto},\n\t}\n\n\tzerolog.Ctx(ctx).Info().Str(\"endpoint\", endpoint).Msg(\"Connecting\")\n\n\tconn, err := quic.DialAddr(ctx, endpoint, tlsConf, DefaultConfig)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tgrpconn, err := bgrpc.DialContext(ctx, endpoint,\n\t\tgrpc.WithBlock(),\n\t\tgrpc.WithTransportCredentials(insecure.NewCredentials()),\n\t\tgrpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {\n\t\t\treturn quicnet.OpenStream(ctx, conn)\n\t\t}),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcli := v1.NewProxyServiceClient(grpconn)\n\n\trsrv, err := cli.Register(metadata.NewOutgoingContext(ctx, md), &v1.RegisterRequest{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\teg, ctx := errgroup.WithContext(ctx)\n\n\teg.Go(func() error {\n\t\tfor {\n\t\t\tstream, err := conn.AcceptStream(ctx)\n\t\t\tif err != nil {\n\t\t\t\tif !errors.Is(err, context.Canceled) {\n\t\t\t\t\tzerolog.Ctx(ctx).Err(err).Msg(\"accept failed\")\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tpconn := proxyproto.NewConn(quicnet.Conn{Stream: stream, Conn: conn})\n\n\t\t\tzerolog.Ctx(ctx).Info().Stringer(\"remote_addr\", pconn.RemoteAddr()).\n\t\t\t\tStringer(\"local_addr\", pconn.LocalAddr()).Msg(\"New remote connection\")\n\n\t\t\tif err := handlers.Proxy(pconn); err != nil {\n\t\t\t\tzerolog.Ctx(ctx).Err(err).Msg(\"handle failed\")\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t})\n\n\teg.Go(func() error {\n\t\tfor {\n\t\t\tmsg, err := rsrv.Recv()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\thandlers.OnAllocation(msg.Endpoint)\n\t\t}\n\t})\n\n\treturn eg.Wait()\n}\n"
  },
  {
    "path": "pkg/sshd/keepalive.go",
    "content": "package sshd\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/gliderlabs/ssh\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc keepAlive(ctx context.Context, logger zerolog.Logger, session ssh.Session) {\n\tt := time.NewTicker(15 * time.Second)\n\tdefer t.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-t.C:\n\t\t\tt := time.Now()\n\t\t\tif _, err := session.SendRequest(\"keepalive@openssh.com\", true, nil); err != nil {\n\t\t\t\tif !errors.Is(err, io.EOF) {\n\t\t\t\t\tlogger.Err(err).Msg(\"Failed to send keepalive\")\n\t\t\t\t} else {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlogger.Debug().Dur(\"took\", time.Since(t)).Msg(\"Got KeepAlive response\")\n\t\t\t}\n\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/sshd/pty_unix.go",
    "content": "//go:build !windows\n\npackage sshd\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"syscall\"\n\t\"unsafe\"\n\n\t\"github.com/creack/pty\"\n\t\"github.com/gliderlabs/ssh\"\n)\n\nfunc handlePty(session io.ReadWriter, ptyReq ssh.Pty, winCh <-chan ssh.Window, cmd *exec.Cmd) error {\n\tcmd.Env = append(cmd.Env, fmt.Sprintf(\"TERM=%s\", ptyReq.Term))\n\tptyFile, err := pty.Start(cmd)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer ptyFile.Close()\n\n\tgo syncWinSize(ptyFile, winCh)\n\tgo func() {\n\t\t_, _ = io.Copy(ptyFile, session) // stdin\n\t}()\n\t_, _ = io.Copy(session, ptyFile) // stdout\n\n\treturn nil\n}\n\nfunc syncWinSize(ptyFile *os.File, winCh <-chan ssh.Window) {\n\tfor win := range winCh {\n\t\tsetWinsize(ptyFile, win.Width, win.Height)\n\t}\n}\n\nfunc setWinsize(f *os.File, w, h int) {\n\tsyscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ),\n\t\tuintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0})))\n}\n"
  },
  {
    "path": "pkg/sshd/pty_windows.go",
    "content": "//go:build windows\n\npackage sshd\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"os/exec\"\n\n\t\"github.com/gliderlabs/ssh\"\n)\n\nfunc handlePty(session io.ReadWriter, ptyReq ssh.Pty, winCh <-chan ssh.Window, cmd *exec.Cmd) error {\n\treturn errors.New(\"pty not supported in windows\")\n}\n"
  },
  {
    "path": "pkg/sshd/sftp.go",
    "content": "package sshd\n\nimport (\n\t\"io\"\n\n\t\"github.com/gliderlabs/ssh\"\n\t\"github.com/pkg/sftp\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc makeSftpHandler(logger zerolog.Logger) ssh.SubsystemHandler {\n\treturn func(sess ssh.Session) {\n\t\tserver, err := sftp.NewServer(sess, sftp.WithDebug(io.Discard))\n\t\tif err != nil {\n\t\t\tlogger.Err(err).Msg(\"sftp: failed to init server\")\n\t\t\treturn\n\t\t}\n\n\t\tdefer server.Close()\n\n\t\tif err := server.Serve(); err != nil && err != io.EOF {\n\t\t\tlogger.Err(err).Msg(\"sftp: session done with error\")\n\t\t} else {\n\t\t\tlogger.Info().Msg(\"sftp: session done\")\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/sshd/sshd.go",
    "content": "package sshd\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/gliderlabs/ssh\"\n\t\"github.com/rs/zerolog\"\n\t\"go.uber.org/atomic\"\n\tgossh \"golang.org/x/crypto/ssh\"\n\t\"golang.org/x/exp/maps\"\n\t\"golang.org/x/exp/slices\"\n)\n\ntype SSHServerOpts struct {\n\tAllowedUsers   []string\n\tAuthorizedKeys map[string]string // Key to owner\n\tEnv            []string\n\tShell          []string\n\tDir            string\n\n\tInteractiveMOTD func(io.Writer)\n}\n\ntype sshKey struct {\n\tKey   ssh.PublicKey\n\tOwner string\n}\n\ntype SSHServer struct {\n\tServer         *ssh.Server\n\tNumConnections func() uint32\n}\n\nfunc MakeServer(ctx context.Context, opts SSHServerOpts) (*SSHServer, error) {\n\tvar authorizedKeys []sshKey\n\tfor key, owner := range opts.AuthorizedKeys {\n\t\tkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tauthorizedKeys = append(authorizedKeys, sshKey{key, owner})\n\t}\n\n\tl := zerolog.Ctx(ctx).With().Str(\"service\", \"sshd\").Logger()\n\n\tconnCount := atomic.NewUint32(0)\n\n\tsrv := &ssh.Server{\n\t\tHandler: func(session ssh.Session) {\n\t\t\tkey, _ := lookupKey(authorizedKeys, session.PublicKey())\n\t\t\tsessionLog := l.With().Stringer(\"remote_addr\", session.RemoteAddr()).Str(\"owner\", key.Owner).Logger()\n\n\t\t\tsessionLog.Info().Str(\"user\", session.User()).Msg(\"incoming ssh session\")\n\n\t\t\targs := opts.Shell[1:]\n\t\t\tif session.RawCommand() != \"\" {\n\t\t\t\tif runtime.GOOS == \"windows\" {\n\t\t\t\t\targs = []string{\"/C\", session.RawCommand()}\n\t\t\t\t} else {\n\t\t\t\t\targs = []string{\"-c\", session.RawCommand()}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcmd := exec.Command(opts.Shell[0], args...)\n\t\t\tcmd.Env = slices.Clone(opts.Env)\n\t\t\tcmd.Dir = opts.Dir\n\n\t\t\tif ssh.AgentRequested(session) {\n\t\t\t\tl, err := ssh.NewAgentListener()\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Fprintf(session, \"Failed to forward agent.\\n\")\n\t\t\t\t} else {\n\t\t\t\t\tdefer l.Close()\n\t\t\t\t\tgo ssh.ForwardAgentConnections(l, session)\n\t\t\t\t\tcmd.Env = append(cmd.Env, fmt.Sprintf(\"%s=%s\", \"SSH_AUTH_SOCK\", l.Addr().String()))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tptyReq, winCh, isPty := session.Pty()\n\n\t\t\tsessionLog.Info().Bool(\"ssh_agent\", ssh.AgentRequested(session)).Bool(\"pty\", isPty).Msg(\"ssh session\")\n\n\t\t\tctx, cancel := context.WithCancel(session.Context())\n\t\t\tdefer cancel()\n\n\t\t\t// Make sure that the connection with the client is kept alive.\n\t\t\tgo keepAlive(ctx, sessionLog, session)\n\n\t\t\tif isPty {\n\t\t\t\t// Print MOTD only if no command was provided\n\t\t\t\tif opts.InteractiveMOTD != nil && session.RawCommand() == \"\" {\n\t\t\t\t\topts.InteractiveMOTD(session)\n\t\t\t\t}\n\n\t\t\t\tif err := handlePty(session, ptyReq, winCh, cmd); err != nil {\n\t\t\t\t\tsessionLog.Err(err).Msg(\"pty start failed\")\n\t\t\t\t\tsession.Exit(1)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcmd.Stdout = session\n\t\t\t\tcmd.Stderr = session\n\t\t\t\tif err := cmd.Start(); err != nil {\n\t\t\t\t\tsessionLog.Err(err).Msg(\"start failed\")\n\t\t\t\t\tsession.Exit(1)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// XXX pass exit code to caller?\n\t\t\terr := cmd.Wait()\n\t\t\tsessionLog.Info().Err(err).Msg(\"ssh session end\")\n\t\t},\n\n\t\tSessionRequestCallback: func(sess ssh.Session, requestType string) bool {\n\t\t\treturn len(opts.AllowedUsers) == 0 || slices.Contains(opts.AllowedUsers, sess.User())\n\t\t},\n\n\t\tPublicKeyHandler: func(ctx ssh.Context, key ssh.PublicKey) bool {\n\t\t\t_, allowed := lookupKey(authorizedKeys, key)\n\t\t\treturn allowed\n\t\t},\n\n\t\tLocalPortForwardingCallback: func(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {\n\t\t\tsessionLog := l.With().Stringer(\"remote_addr\", ctx.RemoteAddr()).Logger()\n\t\t\tsessionLog.Info().Str(\"dst\", fmt.Sprintf(\"%s:%d\", destinationHost, destinationPort)).Msg(\"Port forward request\")\n\t\t\treturn true\n\t\t},\n\n\t\tSubsystemHandlers: map[string]ssh.SubsystemHandler{\n\t\t\t\"sftp\": makeSftpHandler(l),\n\t\t},\n\n\t\tConnCallback: func(ctx ssh.Context, conn net.Conn) net.Conn {\n\t\t\tconnCount.Inc()\n\t\t\tgo func() {\n\t\t\t\t<-ctx.Done()\n\t\t\t\tconnCount.Dec()\n\t\t\t}()\n\n\t\t\treturn conn\n\t\t},\n\t}\n\n\tsrv.ChannelHandlers = maps.Clone(ssh.DefaultChannelHandlers)\n\tsrv.ChannelHandlers[\"direct-tcpip\"] = ssh.DirectTCPIPHandler\n\n\tt := time.Now()\n\tkey, err := rsa.GenerateKey(rand.Reader, 2048)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsigner, err := gossh.NewSignerFromKey(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsrv.HostSigners = append(srv.HostSigners, signer)\n\n\tzerolog.Ctx(ctx).Info().Str(\"host_key_fingerprint\", gossh.FingerprintSHA256(signer.PublicKey())).Dur(\"took\", time.Since(t)).Msg(\"Generated ssh host key\")\n\n\treturn &SSHServer{\n\t\tServer:         srv,\n\t\tNumConnections: connCount.Load,\n\t}, nil\n}\n\nfunc lookupKey(allowed []sshKey, key ssh.PublicKey) (sshKey, bool) {\n\tfor _, allowed := range allowed {\n\t\tif ssh.KeysEqual(key, allowed.Key) {\n\t\t\treturn allowed, true\n\t\t}\n\t}\n\treturn sshKey{}, false\n}\n"
  },
  {
    "path": "pkg/tlscerts/tlscerts.go",
    "content": "package tlscerts\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"math/big\"\n\t\"net\"\n\t\"time\"\n)\n\ntype Subjects struct {\n\tDNSNames    []string\n\tIPAddresses []net.IP\n}\n\nfunc GenerateECDSAPair(subjects Subjects, duration time.Duration) ([]byte, []byte, error) {\n\tserial, err := newSerialNumber()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tpriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tprivDer, err := x509.MarshalPKCS8PrivateKey(priv)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tprivPem := pem.EncodeToMemory(&pem.Block{Type: \"PRIVATE KEY\", Bytes: privDer})\n\n\ttemplate := &x509.Certificate{\n\t\tSerialNumber: serial,\n\t\tNotAfter:     time.Now().Add(duration),\n\t\tDNSNames:     subjects.DNSNames,\n\t\tIPAddresses:  subjects.IPAddresses,\n\t}\n\n\tcertDer, err := x509.CreateCertificate(rand.Reader, template, template, priv.Public(), priv)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tcertPem := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: certDer})\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\treturn certPem, privPem, nil\n}\n\nfunc newSerialNumber() (*big.Int, error) {\n\tserialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)\n\treturn rand.Int(rand.Reader, serialNumberLimit)\n}\n"
  },
  {
    "path": "pkg/waiter/output.go",
    "content": "package waiter\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/muesli/reflow/wordwrap\"\n)\n\nfunc PrintConnectionInfo(endpoint string, deadline time.Time, output io.Writer) {\n\thost, port, _ := net.SplitHostPort(endpoint)\n\n\tif host == \"\" && port == \"\" {\n\t\treturn\n\t}\n\n\tww := wordwrap.NewWriter(80)\n\tfmt.Fprintf(ww, \"Breakpoint! Running until %v (%v).\", deadline.Format(Stamp), humanize.Time(deadline))\n\t_ = ww.Close()\n\n\tlines := strings.Split(ww.String(), \"\\n\")\n\n\tlongestLine := 0\n\tfor _, l := range lines {\n\t\tif len(l) > longestLine {\n\t\t\tlongestLine = len(l)\n\t\t}\n\t}\n\n\tlongline := nchars('─', longestLine)\n\tspaces := nchars(' ', longestLine)\n\tfmt.Fprintln(output)\n\tfmt.Fprintf(output, \"┌─%s─┐\\n\", longline)\n\tfor _, l := range lines {\n\t\tfmt.Fprintf(output, \"│ %s%s │\\n\", l, spaces[len(l):])\n\t}\n\tfmt.Fprintf(output, \"└─%s─┘\\n\", longline)\n\tfmt.Fprintln(output)\n\n\tfmt.Fprintf(output, \"Connect with:\\n\\n\")\n\tfmt.Fprintf(output, \"ssh -p %s runner@%s\\n\", port, host)\n}\n"
  },
  {
    "path": "pkg/waiter/slackbot.go",
    "content": "package waiter\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/google/go-github/v52/github\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/slack-go/slack\"\n\tv1 \"namespacelabs.dev/breakpoint/api/private/v1\"\n\t\"namespacelabs.dev/breakpoint/pkg/jsonfile\"\n)\n\ntype botInstance struct {\n\tclient      *slack.Client\n\tm           *Manager\n\tgithubProps renderGitHubProps\n\n\tchannelID string\n\tts        string\n}\n\nfunc startBot(ctx context.Context, m *Manager, conf v1.SlackBot) *botInstance {\n\tbot := &botInstance{\n\t\tclient:      slack.New(os.ExpandEnv(conf.Token)),\n\t\tm:           m,\n\t\tgithubProps: prepareGitHubProps(ctx),\n\t}\n\n\tchid, ts, err := bot.client.PostMessageContext(ctx, os.ExpandEnv(conf.Channel), bot.makeBlocks(false))\n\tif err != nil {\n\t\tzerolog.Ctx(ctx).Err(err).Msg(\"SlackBot failed\")\n\t\treturn nil\n\t}\n\n\tbot.channelID = chid\n\tbot.ts = ts\n\n\tgo bot.loop(ctx)\n\n\treturn bot\n}\n\nfunc (b *botInstance) Close() error {\n\tctx, done := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer done()\n\n\treturn b.sendUpdate(ctx, true)\n}\n\nfunc (b *botInstance) makeBlocks(leaving bool) slack.MsgOption {\n\tif leaving {\n\t\treturn slack.MsgOptionBlocks(renderGitHubMessage(b.githubProps, \"\", time.Time{})...)\n\t}\n\n\treturn slack.MsgOptionBlocks(renderGitHubMessage(b.githubProps, b.m.Endpoint(), b.m.Expiration())...)\n}\n\nfunc (b *botInstance) sendUpdate(ctx context.Context, leaving bool) error {\n\t_, _, _, err := b.client.UpdateMessageContext(ctx, b.channelID, b.ts, b.makeBlocks(leaving))\n\treturn err\n}\n\nfunc (b *botInstance) loop(ctx context.Context) error {\n\tt := time.NewTicker(30 * time.Second)\n\tdefer t.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\n\t\tcase <-t.C:\n\t\t\tif err := b.sendUpdate(ctx, false); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n}\n\ntype renderGitHubProps struct {\n\tRepository string\n\tRefName    string\n\tWorkflow   string\n\tRunID      string\n\tRunNumber  string\n\tActor      string\n\tPushEvent  *github.PushEvent // Only set on push events.\n}\n\nfunc prepareGitHubProps(ctx context.Context) renderGitHubProps {\n\tprops := renderGitHubProps{\n\t\tRepository: os.Getenv(\"GITHUB_REPOSITORY\"),\n\t\tRefName:    os.Getenv(\"GITHUB_REF_NAME\"),\n\t\tWorkflow:   os.Getenv(\"GITHUB_WORKFLOW\"),\n\t\tRunID:      os.Getenv(\"GITHUB_RUN_ID\"),\n\t\tRunNumber:  os.Getenv(\"GITHUB_RUN_NUMBER\"),\n\t\tActor:      os.Getenv(\"GITHUB_ACTOR\"),\n\t}\n\n\tif eventFile := os.Getenv(\"GITHUB_EVENT_PAH\"); os.Getenv(\"GITHUB_EVENT_NAME\") == \"push\" && eventFile != \"\" {\n\t\tvar pushEvent github.PushEvent\n\t\tif err := jsonfile.Load(eventFile, &pushEvent); err != nil {\n\t\t\tzerolog.Ctx(ctx).Warn().Err(err).Msg(\"Failed to load event file\")\n\t\t} else {\n\t\t\tprops.PushEvent = &pushEvent\n\t\t}\n\t}\n\n\treturn props\n}\n\nfunc renderGitHubMessage(props renderGitHubProps, endpoint string, exp time.Time) []slack.Block {\n\tblocks := []slack.Block{\n\t\tslack.NewHeaderBlock(slack.NewTextBlockObject(slack.PlainTextType, \"Workflow failed\", false, false)),\n\t\tslack.NewSectionBlock(slack.NewTextBlockObject(\n\t\t\tslack.MarkdownType,\n\t\t\tfmt.Sprintf(\"*Repository:* <https://github.com/%s/tree/%s|github.com/%s> (%s)\", props.Repository, props.RefName, props.Repository, props.RefName),\n\t\t\tfalse, false,\n\t\t), nil, nil),\n\t\tslack.NewSectionBlock(slack.NewTextBlockObject(\n\t\t\tslack.MarkdownType,\n\t\t\tfmt.Sprintf(\"*Workflow:* %s (<https://github.com/%s/actions/runs/%s|Run #%s>)\", props.Workflow, props.Repository, props.RunID, props.RunNumber),\n\t\t\tfalse, false,\n\t\t), nil, nil),\n\t}\n\n\tif props.PushEvent != nil && props.PushEvent.HeadCommit != nil && props.PushEvent.HeadCommit.Message != nil {\n\t\tblocks = append(blocks,\n\t\t\tslack.NewSectionBlock(slack.NewTextBlockObject(\n\t\t\t\tslack.MarkdownType,\n\t\t\t\tfmt.Sprintf(\"*<%s|Commit>:* %s`\", maybeCommitURL(props.Repository, *props.PushEvent), *props.PushEvent.HeadCommit.Message),\n\t\t\t\tfalse, false,\n\t\t\t), nil, nil))\n\t}\n\n\tif endpoint != \"\" && !exp.IsZero() {\n\t\thost, port, _ := net.SplitHostPort(endpoint)\n\n\t\tblocks = append(blocks,\n\t\t\tslack.NewSectionBlock(slack.NewTextBlockObject(\n\t\t\t\tslack.MarkdownType,\n\t\t\t\tfmt.Sprintf(\"*SSH:* `ssh -p %s runner@%s`\", port, host),\n\t\t\t\tfalse, false,\n\t\t\t), nil, nil),\n\t\t\tslack.NewSectionBlock(slack.NewTextBlockObject(\n\t\t\t\tslack.MarkdownType,\n\t\t\t\tfmt.Sprintf(\"*Expires:* %s (%s)\", humanize.Time(exp), exp.Format(Stamp)),\n\t\t\t\tfalse, false,\n\t\t\t), nil, nil),\n\t\t)\n\t}\n\n\tblocks = append(blocks, slack.NewContextBlock(\"\",\n\t\tslack.NewTextBlockObject(slack.PlainTextType, fmt.Sprintf(\"Actor: %s\", props.Actor), false, false)))\n\n\treturn blocks\n}\n\nfunc maybeCommitURL(repo string, event github.PushEvent) string {\n\tif event.HeadCommit == nil || event.HeadCommit.URL == nil {\n\t\tif event.Repo == nil {\n\t\t\treturn \"https://github.com/\" + repo\n\t\t}\n\n\t\treturn *event.Repo.URL\n\t}\n\n\treturn *event.HeadCommit.URL\n}\n"
  },
  {
    "path": "pkg/waiter/template.go",
    "content": "package waiter\n\nimport (\n\t\"os\"\n)\n\nfunc execTemplate(value any, mapping func(string) string) any {\n\tif value == nil {\n\t\treturn nil\n\t}\n\n\tswitch x := value.(type) {\n\tcase map[string]any:\n\t\treturn execMapTemplate(x, mapping)\n\n\tcase string:\n\t\treturn os.Expand(x, mapping)\n\n\tcase []any:\n\t\tvar res []any\n\t\tfor _, y := range x {\n\t\t\tres = append(res, execTemplate(y, mapping))\n\t\t}\n\t\treturn res\n\n\tdefault:\n\t}\n\n\treturn value\n}\n\nfunc execMapTemplate(input map[string]any, mapping func(string) string) map[string]any {\n\tif input == nil {\n\t\treturn nil\n\t}\n\n\tout := map[string]any{}\n\tfor key, value := range input {\n\t\tout[key] = execTemplate(value, mapping)\n\t}\n\n\treturn out\n}\n"
  },
  {
    "path": "pkg/waiter/template_test.go",
    "content": "package waiter\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\tv1 \"namespacelabs.dev/breakpoint/api/private/v1\"\n)\n\nfunc TestExecTemplate(t *testing.T) {\n\tvar webhook v1.Webhook\n\n\tif err := json.Unmarshal([]byte(`{\n\t\"url\": \"foobar\",\n\t\"payload\": {\n        \"blocks\": [\n          {\n            \"type\": \"header\",\n            \"text\": {\n              \"type\": \"plain_text\",\n              \"text\": \"Workflow failed\",\n              \"emoji\": true\n            }\n          },\n          {\n            \"type\": \"section\",\n            \"text\": {\n              \"type\": \"mrkdwn\",\n              \"text\": \"*Repository:* <https://${GITHUB_REPOSITORY}/tree/${GITHUB_REF_NAME}|${GITHUB_REPOSITORY}> (${GITHUB_REF_NAME})\"\n            }\n          }\n\t\t]\n\t}\n}`), &webhook); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tgot := execTemplate(webhook.Payload, func(str string) string {\n\t\tswitch str {\n\t\tcase \"GITHUB_REPOSITORY\":\n\t\t\treturn \"arepo\"\n\n\t\tcase \"GITHUB_REF_NAME\":\n\t\t\treturn \"main\"\n\t\t}\n\n\t\treturn \"\"\n\t})\n\n\tif d := cmp.Diff(map[string]any{\n\t\t\"blocks\": []any{\n\t\t\tmap[string]any{\n\t\t\t\t\"text\": map[string]any{\n\t\t\t\t\t\"emoji\": bool(true),\n\t\t\t\t\t\"text\":  string(\"Workflow failed\"),\n\t\t\t\t\t\"type\":  string(\"plain_text\"),\n\t\t\t\t},\n\t\t\t\t\"type\": string(\"header\"),\n\t\t\t},\n\t\t\tmap[string]any{\n\t\t\t\t\"text\": map[string]any{\n\t\t\t\t\t\"text\": string(\"*Repository:* <https://arepo/tree/main|arepo> (main)\"),\n\t\t\t\t\t\"type\": string(\"mrkdwn\"),\n\t\t\t\t},\n\t\t\t\t\"type\": string(\"section\"),\n\t\t\t},\n\t\t},\n\t}, got); d != \"\" {\n\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", d)\n\t}\n\n}\n"
  },
  {
    "path": "pkg/waiter/waiter.go",
    "content": "package waiter\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"math\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/rs/zerolog\"\n\tv1 \"namespacelabs.dev/breakpoint/api/private/v1\"\n\t\"namespacelabs.dev/breakpoint/pkg/webhook\"\n)\n\nconst (\n\tlogTickInterval = 1 * time.Minute\n\n\tStamp = time.Stamp + \" MST\"\n)\n\ntype ManagerOpts struct {\n\tInitialDur time.Duration\n\n\tWebhooks  []v1.Webhook\n\tSlackBots []v1.SlackBot\n}\n\ntype ManagerStatus struct {\n\tEndpoint       string    `json:\"endpoint\"`\n\tExpiration     time.Time `json:\"expiration\"`\n\tNumConnections uint32    `json:\"num_connections\"`\n}\n\ntype Manager struct {\n\tctx    context.Context\n\tlogger zerolog.Logger\n\n\topts ManagerOpts\n\n\tmu                      sync.Mutex\n\tupdated                 chan struct{}\n\texpiration              time.Time\n\tendpoint                string\n\tresources               []io.Closer\n\tconnectionCountCallback func() uint32\n}\n\nfunc NewManager(ctx context.Context, opts ManagerOpts) (*Manager, context.Context) {\n\tctx, cancel := context.WithCancel(ctx)\n\tl := zerolog.Ctx(ctx).With().Logger()\n\tm := &Manager{\n\t\tctx:        ctx,\n\t\tlogger:     l,\n\t\topts:       opts,\n\t\tupdated:    make(chan struct{}, 1),\n\t\texpiration: time.Now().Add(opts.InitialDur),\n\t}\n\n\tgo func() {\n\t\tdefer cancel()\n\t\tm.loop(ctx)\n\n\t\tm.mu.Lock()\n\t\tresources := m.resources\n\t\tm.resources = nil\n\t\tm.mu.Unlock()\n\n\t\t// Resources should clean up quickly as they hold up the cancelation of the context.\n\t\t// We're guaranteed to wait for these because the incoming `ctx` is never cancelled.\n\t\tfor _, closer := range resources {\n\t\t\tif err := closer.Close(); err != nil {\n\t\t\t\tl.Err(err).Msg(\"Failed while cleaning up resource\")\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn m, ctx\n}\n\nfunc (m *Manager) Wait() error {\n\t<-m.ctx.Done()\n\treturn m.ctx.Err()\n}\n\nfunc (m *Manager) loop(ctx context.Context) {\n\texitTimer := time.NewTicker(time.Until(m.expiration))\n\tdefer exitTimer.Stop()\n\n\tlogTicker := time.NewTicker(logTick())\n\tdefer logTicker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase _, ok := <-m.updated:\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tm.mu.Lock()\n\t\t\tnewExp := m.expiration\n\t\t\tm.mu.Unlock()\n\n\t\t\texitTimer.Reset(time.Until(newExp))\n\t\t\tm.announce()\n\n\t\tcase <-exitTimer.C:\n\t\t\t// Timer has expired, terminate the program\n\t\t\tm.logger.Info().Msg(\"Breakpoint expired\")\n\t\t\treturn\n\n\t\tcase <-logTicker.C:\n\t\t\tm.announce()\n\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc logTick() time.Duration {\n\t// If running in CI, announce on a regular basis.\n\tif os.Getenv(\"CI\") != \"\" {\n\t\treturn logTickInterval\n\t}\n\n\treturn math.MaxInt64\n}\n\nfunc (m *Manager) ExtendWait(dur time.Duration) time.Time {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tm.expiration = m.expiration.Add(dur)\n\n\tm.updated <- struct{}{}\n\n\tm.logger.Info().\n\t\tDur(\"dur\", dur).\n\t\tTime(\"expiration\", m.expiration).\n\t\tMsg(\"Extend wait\")\n\treturn m.expiration\n}\n\nfunc (m *Manager) StopWait() {\n\tm.logger.Info().Msg(\"Resume requested\")\n\tclose(m.updated)\n}\n\nfunc (m *Manager) Expiration() time.Time {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\treturn m.expiration\n}\n\nfunc (m *Manager) Endpoint() string {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\treturn m.endpoint\n}\n\nfunc (m *Manager) Status() ManagerStatus {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\treturn ManagerStatus{\n\t\tEndpoint:       m.endpoint,\n\t\tExpiration:     m.expiration,\n\t\tNumConnections: m.connectionCountCallback(),\n\t}\n}\n\nfunc (m *Manager) SetEndpoint(addr string) {\n\tm.mu.Lock()\n\tm.endpoint = addr\n\tm.mu.Unlock()\n\n\tvar resources []io.Closer\n\tfor _, bot := range m.opts.SlackBots {\n\t\tif bot := startBot(m.ctx, m, bot); bot != nil {\n\t\t\tresources = append(resources, bot)\n\t\t}\n\t}\n\n\tm.mu.Lock()\n\tm.resources = resources\n\tm.mu.Unlock()\n\n\tm.updated <- struct{}{}\n\n\texpandf := expand(addr, m.Expiration())\n\n\tfor _, wh := range m.opts.Webhooks {\n\t\tctx, done := context.WithTimeout(m.ctx, 30*time.Second)\n\t\tdefer done()\n\n\t\tpayload := execTemplate(wh.Payload, expandf)\n\n\t\tt := time.Now()\n\t\tif err := webhook.Notify(ctx, os.Expand(wh.URL, expandf), payload); err != nil {\n\t\t\tm.logger.Err(err).Msg(\"Failed to notify Webhook\")\n\t\t} else {\n\t\t\tm.logger.Info().Dur(\"took\", time.Since(t)).Str(\"url\", wh.URL).Msg(\"Notified webhook\")\n\t\t}\n\t}\n}\n\nfunc (m *Manager) SetConnectionCountCallback(callback func() uint32) {\n\tm.mu.Lock()\n\tm.connectionCountCallback = callback\n\tm.mu.Unlock()\n}\n\nfunc expand(addr string, exp time.Time) func(key string) string {\n\thost, port, _ := net.SplitHostPort(addr)\n\n\treturn func(key string) string {\n\t\tswitch key {\n\t\tcase \"BREAKPOINT_ENDPOINT\":\n\t\t\treturn addr\n\n\t\tcase \"BREAKPOINT_HOST\":\n\t\t\treturn host\n\n\t\tcase \"BREAKPOINT_PORT\":\n\t\t\treturn port\n\n\t\tcase \"BREAKPOINT_TIME_LEFT\":\n\t\t\treturn strings.TrimSpace(humanize.RelTime(exp, time.Now(), \"\", \"\"))\n\n\t\tcase \"BREAKPOINT_EXPIRATION\":\n\t\t\treturn exp.Format(Stamp)\n\t\t}\n\n\t\treturn os.Getenv(key)\n\t}\n}\n\nfunc (m *Manager) announce() {\n\tstatus := m.Status()\n\tPrintConnectionInfo(status.Endpoint, status.Expiration, os.Stderr)\n}\n\nfunc nchars(ch rune, n int) string {\n\tstr := make([]rune, n)\n\tfor k := 0; k < n; k++ {\n\t\tstr[k] = ch\n\t}\n\treturn string(str)\n}\n"
  },
  {
    "path": "pkg/webhook/notifier.go",
    "content": "package webhook\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"namespacelabs.dev/breakpoint/pkg/httperrors\"\n)\n\nconst (\n\tuserAgent = \"Breakpoint/1.0\"\n)\n\nfunc Notify(ctx context.Context, endpoint string, payload any) error {\n\tbody, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(body))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Set(\"User-Agent\", userAgent)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer resp.Body.Close()\n\n\treturn httperrors.MaybeError(resp)\n}\n"
  }
]