Full Code of namespacelabs/breakpoint for AI

main d8f06ac16053 cached
71 files
165.5 KB
57.8k tokens
253 symbols
1 requests
Download .txt
Repository: namespacelabs/breakpoint
Branch: main
Commit: d8f06ac16053
Files: 71
Total size: 165.5 KB

Directory structure:
gitextract_v5sic7m5/

├── .dockerignore
├── .github/
│   ├── slack-notification.json
│   └── workflows/
│       ├── build.yml
│       ├── checks.yml
│       ├── goreleaser.yml
│       └── release.yml
├── .gitignore
├── .goreleaser.yaml
├── Dockerfile
├── LICENSE
├── README.md
├── api/
│   ├── private/
│   │   └── v1/
│   │       ├── configtype.go
│   │       ├── service.pb.go
│   │       ├── service.proto
│   │       └── service_grpc.pb.go
│   └── public/
│       └── v1/
│           ├── service.pb.go
│           ├── service.proto
│           ├── service_grpc.pb.go
│           └── types.go
├── buf.gen.yaml
├── cmd/
│   ├── breakpoint/
│   │   ├── attach.go
│   │   ├── extend.go
│   │   ├── hold.go
│   │   ├── main.go
│   │   ├── resume.go
│   │   ├── start.go
│   │   ├── status.go
│   │   └── wait.go
│   └── rendezvous/
│       └── main.go
├── docs/
│   ├── CONTRIBUTING.md
│   └── server-setup.md
├── examples/
│   └── wait.withslack.json
├── flake.nix
├── fly.toml
├── go.mod
├── go.sum
└── pkg/
    ├── README.md
    ├── bcontrol/
    │   └── client.go
    ├── bgrpc/
    │   └── bgrpc.go
    ├── blog/
    │   └── blog.go
    ├── config/
    │   └── config.go
    ├── execbackground/
    │   ├── bg_unix.go
    │   └── bg_windows.go
    ├── github/
    │   └── sshkeys.go
    ├── githuboidc/
    │   ├── claims.go
    │   ├── gh.go
    │   └── verifier.go
    ├── httperrors/
    │   └── httperrors.go
    ├── internalserver/
    │   └── internalserver.go
    ├── jsonfile/
    │   └── load.go
    ├── passthrough/
    │   └── listener.go
    ├── quicgrpc/
    │   └── grpccreds.go
    ├── quicnet/
    │   ├── conn.go
    │   └── listener.go
    ├── quicproxy/
    │   ├── proxyproto.go
    │   ├── rawproto.go
    │   ├── serve.go
    │   └── service.go
    ├── quicproxyclient/
    │   └── client.go
    ├── sshd/
    │   ├── keepalive.go
    │   ├── pty_unix.go
    │   ├── pty_windows.go
    │   ├── sftp.go
    │   └── sshd.go
    ├── tlscerts/
    │   └── tlscerts.go
    ├── waiter/
    │   ├── output.go
    │   ├── slackbot.go
    │   ├── template.go
    │   ├── template_test.go
    │   └── waiter.go
    └── webhook/
        └── notifier.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
fly.toml

================================================
FILE: .github/slack-notification.json
================================================
{
  "url": "${SLACK_WEBHOOK_URL}",
  "payload": {
    "blocks": [
      {
        "type": "header",
        "text": {
          "type": "plain_text",
          "text": "Workflow breakpoint started",
          "emoji": true
        }
      },
      {
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": "*Repository:* <https://github.com/${GITHUB_REPOSITORY}/tree/${GITHUB_REF_NAME}|${GITHUB_REPOSITORY}> (${GITHUB_REF_NAME})"
        }
      },
      {
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": "*Workflow:* ${GITHUB_WORKFLOW} (<https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}|Run #${GITHUB_RUN_NUMBER}>)"
        }
      },
      {
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": "*SSH:* `ssh -p ${BREAKPOINT_PORT} runner@${BREAKPOINT_HOST}`"
        }
      },
      {
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": "*Expires:* in ${BREAKPOINT_TIME_LEFT} (${BREAKPOINT_EXPIRATION})"
        }
      },
      {
        "type": "context",
        "elements": [
          {
            "type": "plain_text",
            "text": "Actor: ${GITHUB_ACTOR}",
            "emoji": true
          }
        ]
      }
    ]
  }
}


================================================
FILE: .github/workflows/build.yml
================================================
name: Build
on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read # Checkout the code
  packages: write # Push to GitHub registry

env:
  IMAGE_NAME: rendezvous
  IMAGE_REPO: ghcr.io/${{ github.repository_owner }}
  VERSION: ${{ github.sha }}

jobs:
  docker-build:
    name: Build with Docker
    runs-on: nscloud
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Setup Buildx for Docker build
        uses: docker/setup-buildx-action@v2

      - name: Docker build the Rendezvous server
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: ${{ env.IMAGE_REPO }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}

      - name: Breakpoint on failure
        if: failure()
        uses: namespacelabs/breakpoint-action@v0
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
        with:
          duration: 30m
          authorized-users: hugosantos,n-g,htr
          slack-announce-channel: "#ci"


================================================
FILE: .github/workflows/checks.yml
================================================
name: Commit Checks
on:
  pull_request:
    branches:
      - "*"
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read # Checkout the code

jobs:
  checks:
    name: Code Checks
    runs-on: nscloud-ubuntu-22.04-amd64-2x8
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Install Go
        uses: actions/setup-go@v4
        with:
          go-version: 'stable'

      - name: Check Go formatting
        run: go fmt ./... && git diff --exit-code

      - name: Check Go mod is tidy
        run: go mod tidy && git diff --exit-code

      - name: Check that Go builds
        run: |
          go build -o . ./cmd/...

      - name: Breakpoint on failure
        if: failure()
        uses: namespacelabs/breakpoint-action@v0
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
        with:
          duration: 30m
          authorized-users: hugosantos,n-g,htr
          slack-announce-channel: "#ci"


================================================
FILE: .github/workflows/goreleaser.yml
================================================
name: Release Binaries

on:
  push:
    tags: ["v*"]

jobs:
  goreleaser:
    runs-on: nscloud-ubuntu-24.04-amd64-4x8
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v6

      - name: Set up Go
        uses: actions/setup-go@v6
        with:
          go-version: "1.20"

      - uses: goreleaser/goreleaser-action@v6
        with:
          args: release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/release.yml
================================================
name: Release Rendezvous Docker image

on:
  release:
    types: [released]

permissions:
  contents: read # Checkout the code
  packages: write # Push to GitHub registry

env:
  IMAGE_NAME: rendezvous
  IMAGE_REPO: ghcr.io/${{ github.repository_owner }}
  VERSION: ${{ github.event.release.tag_name }}

jobs:
  docker-release:
    name: Release Docker image ${{ github.event.release.tag_name }}
    runs-on: nscloud-ubuntu-22.04-amd64-2x8
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Setup Buildx for Docker build
        uses: namespacelabs/nscloud-setup-buildx-action@v0

      - name: Docker build the Rendezvous server
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: ${{ env.IMAGE_REPO }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}
          platforms: linux/amd64,linux/arm64

      - name: Breakpoint on failure
        if: failure()
        uses: namespacelabs/breakpoint-action@v0
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
        with:
          duration: 30m
          authorized-users: hugosantos,n-g,htr
          slack-announce-channel: "#ci"


================================================
FILE: .gitignore
================================================
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work

dist/


================================================
FILE: .goreleaser.yaml
================================================
before:
  hooks:
    # You may remove this if you don't use go modules.
    - go mod tidy
builds:
  - id: breakpoint
    main: ./cmd/breakpoint
    binary: breakpoint
    env:
      - CGO_ENABLED=0
    goos:
      - linux
      - darwin
      - windows
    goarch:
      - amd64
      - arm64

archives:
  - id: breakpoint
    builds:
      - breakpoint
    name_template: "breakpoint_{{ .Os }}_{{ .Arch }}"

release:
  github:
    owner: namespacelabs
    name: breakpoint

checksum:
  name_template: "checksums.txt"
snapshot:
  name_template: "{{ incpatch .Version }}-next"

changelog:
  sort: asc
  filters:
    exclude:
      - "^docs:"
      - "^test:"
      - "^nochangelog"
      - "^Merge pull request"
      - "^Merge branch"


================================================
FILE: Dockerfile
================================================
FROM golang:1.20-alpine AS builder

WORKDIR /app

COPY go.mod ./
COPY go.sum ./
RUN go mod download

COPY . .

RUN go build ./cmd/rendezvous

FROM cgr.dev/chainguard/static

COPY --from=builder /app/rendezvous /rendezvous

CMD [ "/rendezvous" ]

================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
<img src="https://raw.githubusercontent.com/namespacelabs/breakpoint/main/docs/imgs/breakpoint-banner.png" alt="Breakpoint. Debug with SSH. Resume." width="400" height="200">

[![Discord](https://img.shields.io/badge/Join-Namespace-blue?color=blue&label=Discord&logo=discord&logoColor=3eb0ff&style=flat-square)](https://discord.gg/DqMzDFR6Hc)
[![Twitter Follow](https://img.shields.io/badge/Follow-Namespace_Labs-blue?logo=twitter&style=flat-square)](https://twitter.com/intent/follow?screen_name=namespacelabs)
[![GitHub Actions](https://img.shields.io/badge/GitHub-Action-blue?logo=githubactions&style=flat-square)](https://github.com/namespacelabs/breakpoint-action)
![GitHub](https://img.shields.io/github/license/namespacelabs/breakpoint?color=blue&label=License&style=flat-square)
![Build](https://img.shields.io/github/actions/workflow/status/namespacelabs/breakpoint/build.yml?label=Build&style=flat-square)
![Checks](https://img.shields.io/github/actions/workflow/status/namespacelabs/breakpoint/checks.yml?label=Checks&style=flat-square)

# Breakpoint

Add breakpoints to CI (e.g. GitHub Action workflows): pause workflows, access the workflow with SSH, debug and resume executions.

## What is Breakpoint

Have 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?

Breakpoint 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).

You can make changes, re-run commands, and resume the workflow as needed. Need more time? Just run `breakpoint extend` to extend your breakpoint duration.

And it's 100% open-source (both client and server).

> ℹ️ Workflows that have active breakpoints are still "running" and continue to count towards your total CI usage.

## Using Breakpoint

Breakpoint 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.

The 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".

```yaml
jobs:
  go-tests:
    runs-on: ubuntu-latest

    permissions:
      id-token: write
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Run Go tests
        runs: |
          go test ./...

      - name: Breakpoint if tests failed
        if: failure()
        uses: namespacelabs/breakpoint-action@v0
        with:
          duration: 30m
          authorized-users: jack123, alice321
```

When Breakpoint activates, it will output on a regular basis how much time left
there is in the breakpoint, and which address to SSH to get to the workflow.

```bash
┌───────────────────────────────────────────────────────────────────────────┐
│                                                                           │
│ Breakpoint running until 2023-05-24T16:06:48+02:00 (29 minutes from now). │
│                                                                           │
│ Connect with: ssh -p 40812 runner@rendezvous.namespace.so                 │
│                                                                           │
└───────────────────────────────────────────────────────────────────────────┘
```

You can now SSH the runner, re-run builds or tests, and even do changes.

If you need more time, run `breakpoint extend` to extend the breakpoint duration
by 30 more minutes (or extend by more with the `--for` flag).

When you are done, you can end the breakpoint session with `breakpoint resume`.

> [!TIP]
> You can also run Breakpoint in the background, by adding `mode: background` to the actions inputs. \
> That way, you can connect to it at any time during your workflow
> 
> <details>
>  <summary>Example</summary>
>  <p>
>
> ```yaml
>       - name: Breakpoint in the background
>        uses: namespacelabs/breakpoint-action@v0
>        with:
>          mode: background
>          authorized-users: jack123, alice321
> ```
> </p></details>
>
> [More info](https://github.com/namespacelabs/breakpoint-action?tab=readme-ov-file#run-in-the-background)

By default, the Breakpoint Action uses a shared `rendezvous` server provided by
Namespace Labs for free. Even though a shared server is used, your SSH traffic is always _encrypted end-to-end_ (see Architecture).

Check out the [Breakpoint Action](https://github.com/namespacelabs/breakpoint-action) for more details on
what arguments you can set.

### Using the Breakpoint CLI to create a breakpoint

To activate a breakpoint, you can run:

```bash
$ breakpoint wait --config config.json
```

The config file can look like as follows:

```json
{
  "endpoint": "rendezvous.namespace.so:5000",
  "shell": ["/bin/bash"],
  "allowed_ssh_users": ["runner"],
  "authorized_keys": [],
  "authorized_github_users": ["<your-github-username>"],
  "duration": "30m"
}
```

The `wait` command will block the caller and print an SSH endpoint that you can connect to:

```bash
┌───────────────────────────────────────────────────────────────────────────┐
│                                                                           │
│ Breakpoint running until 2023-05-24T16:06:48+02:00 (29 minutes from now). │
│                                                                           │
│ Connect with: ssh -p 40812 runner@rendezvous.namespace.so                 │
│                                                                           │
└───────────────────────────────────────────────────────────────────────────┘
```

Once 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`):

- `breakpoint extend --for 60m`: extend the wait period for 30m more minutes
- `breakpoint resume`: stops Breakpoint process and release the control flow to the caller of the `wait` command

## Architecture

Breakpoint consists of two main components: `rendezvous` (where public connections are terminated) and `breakpoint`.

When a breakpoint is created, the CLI blocks until an expiration time has passed.

Meanwhile, it establishes a QUIC connection to `rendezvous`, which allocates a
public endpoint (with a random port) that will be reverse proxied back to the
running `breakpoint`; each connection then serves a SSH session (from a ssh
service embedded in `breakpoint`). SSH sessions do not start new user sessions,
and always run commands using the same uid as the parent `breakpoint wait` as
well.

The first QUIC stream `breakpoint -> rendezvous` is used for gRPC; `rendezvous`
expects a `Register` stream in order to allocate an endpoint, and will serve
that endpoint while the corresponding gRPC stream is active.

Because the SSH session is established end-to-end, `rendezvous` is not capable of performing a man-in-the-middle attack.

![architecture](docs/imgs/Breakpoint%20high-level%20view.png)

The CLI implements pausing by blocking the caller process. The command
`breakpoint wait` blocks until either the user runs `breakpoint resume` or the
wait-timer expires. The communication between the `wait` process and the CLI is
implemented with gRPC.

On receive a connection, `rendezvous` establishes a new QUIC stream over the
same connection that was registered previously, in the direction `rendezvous -> breakpoint` and performs dumb TCP proxying over it, without the need of additional framing.

The lack of additional framing in addition to QUIC's streams having independent
control flow (i.e. no shared head of the line blocking), make QUIC a perfect
solution for this type of reverse proxying (in fact, cloudflare uses similar
techniques in Cloudflare Tunnel).

## Authentication

The 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).

You 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.

The 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.

For example, the following `config.json` allows access to "jack123" and "alice321" GitHub users with a SSH user called "runner".

```json
{
  "allowed_ssh_users": ["runner"],
  "authorized_github_users": ["jack123", "alice321"]
}
```

### GitHub-based authentication (via OIDC)

`breakpoint` is able to request a fresh GitHub-emitted workflow identifying token, that it sends to `rendezvous`.

`rendezvous` has the ability to verify these, and performs access control based on the repository where the invocation was originated.

Even if no access control is enforced, repository information is logged by `rendezvous` if available.

## Using Namespace's shared Rendezvous

Namespace Labs runs a public `rendezvous` server that is open to everyone. But you can also run your own (see below).

Although `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.

To use the shared `rendezvous`, use the following endpoint:

```json
{
  "endpoint": "rendezvous.namespace.so:5000"
}
```

## Running Rendezvous yourself

See our [documentation](docs/server-setup.md) on how to run your own instance of `rendezvous`.

## Roadmap

Here's a list of features that we'd to tackle but haven't gotten to yet.

1. 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.
2. The Rendezvous Server does not implement a control and monitoring Web UI.
3. Neither the Rendezvous Server nor the Breakpoint client expose metrics.
4. The Breakpoint session does not automatically extend itself if an SSH connection is active. You need to explicitly extend the session with `breakpoint extend`.
5. Configurable ACLs on the Rendezvous Server to specify the list of repositories and organizations allowed to connect to the server.
6. Support for more authentication schemes between `breakpoint` and `rendezvous`. Breakpoint client and Rendezvous Server only support GitHub's OIDC-based authentication today.
7. Team and Organization authorization of users in Breakpoint client's SSH service (i.e. specifying a team or org rather than individual usernames).

## Contributions

Breakpoint welcomes your help! We appreciate your time and effort.

If 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.

Check out our [contribution guidelines](docs/CONTRIBUTING.md) for more details on how to develop Breakpoint.

## Join the Community

If you have questions, ideas or feedback, chat with the team on our [Discord server](https://community.namespace.so/discord).


================================================
FILE: api/private/v1/configtype.go
================================================
package v1

type WaitConfig struct {
	Endpoint              string    `json:"endpoint"`
	Duration              string    `json:"duration"`
	AuthorizedKeys        []string  `json:"authorized_keys"`
	AuthorizedGithubUsers []string  `json:"authorized_github_users"`
	Shell                 []string  `json:"shell"`
	AllowedSSHUsers       []string  `json:"allowed_ssh_users"`
	Enable                []string  `json:"enable"`
	Webhooks              []Webhook `json:"webhooks"`
	SlackBot              *SlackBot `json:"slack_bot"`
}

type Webhook struct {
	URL     string         `json:"url"`
	Payload map[string]any `json:"payload"`
}

type SlackBot struct {
	Token   string `json:"token"`
	Channel string `json:"channel"`
}


================================================
FILE: api/private/v1/service.pb.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// 	protoc-gen-go v1.29.1
// 	protoc        (unknown)
// source: api/private/v1/service.proto

package v1

import (
	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
	durationpb "google.golang.org/protobuf/types/known/durationpb"
	emptypb "google.golang.org/protobuf/types/known/emptypb"
	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
	reflect "reflect"
	sync "sync"
)

const (
	// Verify that this generated code is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
	// Verify that runtime/protoimpl is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)

type ExtendRequest struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	WaitFor *durationpb.Duration `protobuf:"bytes,1,opt,name=wait_for,json=waitFor,proto3" json:"wait_for,omitempty"`
}

func (x *ExtendRequest) Reset() {
	*x = ExtendRequest{}
	if protoimpl.UnsafeEnabled {
		mi := &file_api_private_v1_service_proto_msgTypes[0]
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		ms.StoreMessageInfo(mi)
	}
}

func (x *ExtendRequest) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*ExtendRequest) ProtoMessage() {}

func (x *ExtendRequest) ProtoReflect() protoreflect.Message {
	mi := &file_api_private_v1_service_proto_msgTypes[0]
	if protoimpl.UnsafeEnabled && x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use ExtendRequest.ProtoReflect.Descriptor instead.
func (*ExtendRequest) Descriptor() ([]byte, []int) {
	return file_api_private_v1_service_proto_rawDescGZIP(), []int{0}
}

func (x *ExtendRequest) GetWaitFor() *durationpb.Duration {
	if x != nil {
		return x.WaitFor
	}
	return nil
}

type ExtendResponse struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	Expiration *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=expiration,proto3" json:"expiration,omitempty"`
}

func (x *ExtendResponse) Reset() {
	*x = ExtendResponse{}
	if protoimpl.UnsafeEnabled {
		mi := &file_api_private_v1_service_proto_msgTypes[1]
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		ms.StoreMessageInfo(mi)
	}
}

func (x *ExtendResponse) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*ExtendResponse) ProtoMessage() {}

func (x *ExtendResponse) ProtoReflect() protoreflect.Message {
	mi := &file_api_private_v1_service_proto_msgTypes[1]
	if protoimpl.UnsafeEnabled && x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use ExtendResponse.ProtoReflect.Descriptor instead.
func (*ExtendResponse) Descriptor() ([]byte, []int) {
	return file_api_private_v1_service_proto_rawDescGZIP(), []int{1}
}

func (x *ExtendResponse) GetExpiration() *timestamppb.Timestamp {
	if x != nil {
		return x.Expiration
	}
	return nil
}

type StatusResponse struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	Expiration     *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=expiration,proto3" json:"expiration,omitempty"`
	Endpoint       string                 `protobuf:"bytes,2,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
	NumConnections uint32                 `protobuf:"varint,3,opt,name=num_connections,json=numConnections,proto3" json:"num_connections,omitempty"`
}

func (x *StatusResponse) Reset() {
	*x = StatusResponse{}
	if protoimpl.UnsafeEnabled {
		mi := &file_api_private_v1_service_proto_msgTypes[2]
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		ms.StoreMessageInfo(mi)
	}
}

func (x *StatusResponse) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*StatusResponse) ProtoMessage() {}

func (x *StatusResponse) ProtoReflect() protoreflect.Message {
	mi := &file_api_private_v1_service_proto_msgTypes[2]
	if protoimpl.UnsafeEnabled && x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use StatusResponse.ProtoReflect.Descriptor instead.
func (*StatusResponse) Descriptor() ([]byte, []int) {
	return file_api_private_v1_service_proto_rawDescGZIP(), []int{2}
}

func (x *StatusResponse) GetExpiration() *timestamppb.Timestamp {
	if x != nil {
		return x.Expiration
	}
	return nil
}

func (x *StatusResponse) GetEndpoint() string {
	if x != nil {
		return x.Endpoint
	}
	return ""
}

func (x *StatusResponse) GetNumConnections() uint32 {
	if x != nil {
		return x.NumConnections
	}
	return 0
}

var File_api_private_v1_service_proto protoreflect.FileDescriptor

var file_api_private_v1_service_proto_rawDesc = []byte{
	0x0a, 0x1c, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31,
	0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x20,
	0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x6c, 0x61, 0x62, 0x73, 0x2e, 0x62, 0x72,
	0x65, 0x61, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65,
	0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
	0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
	0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
	0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67,
	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74,
	0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x45,
	0x0a, 0x0d, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
	0x34, 0x0a, 0x08, 0x77, 0x61, 0x69, 0x74, 0x5f, 0x66, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28,
	0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
	0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x77, 0x61,
	0x69, 0x74, 0x46, 0x6f, 0x72, 0x22, 0x4c, 0x0a, 0x0e, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x52,
	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72,
	0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
	0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,
	0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74,
	0x69, 0x6f, 0x6e, 0x22, 0x91, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65,
	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61,
	0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
	0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
	0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69,
	0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x02,
	0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x27,
	0x0a, 0x0f, 0x6e, 0x75, 0x6d, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e,
	0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x6e, 0x75, 0x6d, 0x43, 0x6f, 0x6e, 0x6e,
	0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x8b, 0x02, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x74,
	0x72, 0x6f, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x38, 0x0a, 0x06, 0x52, 0x65,
	0x73, 0x75, 0x6d, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
	0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67,
	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
	0x6d, 0x70, 0x74, 0x79, 0x12, 0x6b, 0x0a, 0x06, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x12, 0x2f,
	0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x6c, 0x61, 0x62, 0x73, 0x2e, 0x62,
	0x72, 0x65, 0x61, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74,
	0x65, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
	0x30, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x6c, 0x61, 0x62, 0x73, 0x2e,
	0x62, 0x72, 0x65, 0x61, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61,
	0x74, 0x65, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
	0x65, 0x12, 0x52, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f,
	0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
	0x70, 0x74, 0x79, 0x1a, 0x30, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x6c,
	0x61, 0x62, 0x73, 0x2e, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x70,
	0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73,
	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2d, 0x5a, 0x2b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61,
	0x63, 0x65, 0x6c, 0x61, 0x62, 0x73, 0x2e, 0x64, 0x65, 0x76, 0x2f, 0x62, 0x72, 0x65, 0x61, 0x6b,
	0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74,
	0x65, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}

var (
	file_api_private_v1_service_proto_rawDescOnce sync.Once
	file_api_private_v1_service_proto_rawDescData = file_api_private_v1_service_proto_rawDesc
)

func file_api_private_v1_service_proto_rawDescGZIP() []byte {
	file_api_private_v1_service_proto_rawDescOnce.Do(func() {
		file_api_private_v1_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_private_v1_service_proto_rawDescData)
	})
	return file_api_private_v1_service_proto_rawDescData
}

var file_api_private_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_api_private_v1_service_proto_goTypes = []interface{}{
	(*ExtendRequest)(nil),         // 0: namespacelabs.breakpoint.private.ExtendRequest
	(*ExtendResponse)(nil),        // 1: namespacelabs.breakpoint.private.ExtendResponse
	(*StatusResponse)(nil),        // 2: namespacelabs.breakpoint.private.StatusResponse
	(*durationpb.Duration)(nil),   // 3: google.protobuf.Duration
	(*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp
	(*emptypb.Empty)(nil),         // 5: google.protobuf.Empty
}
var file_api_private_v1_service_proto_depIdxs = []int32{
	3, // 0: namespacelabs.breakpoint.private.ExtendRequest.wait_for:type_name -> google.protobuf.Duration
	4, // 1: namespacelabs.breakpoint.private.ExtendResponse.expiration:type_name -> google.protobuf.Timestamp
	4, // 2: namespacelabs.breakpoint.private.StatusResponse.expiration:type_name -> google.protobuf.Timestamp
	5, // 3: namespacelabs.breakpoint.private.ControlService.Resume:input_type -> google.protobuf.Empty
	0, // 4: namespacelabs.breakpoint.private.ControlService.Extend:input_type -> namespacelabs.breakpoint.private.ExtendRequest
	5, // 5: namespacelabs.breakpoint.private.ControlService.Status:input_type -> google.protobuf.Empty
	5, // 6: namespacelabs.breakpoint.private.ControlService.Resume:output_type -> google.protobuf.Empty
	1, // 7: namespacelabs.breakpoint.private.ControlService.Extend:output_type -> namespacelabs.breakpoint.private.ExtendResponse
	2, // 8: namespacelabs.breakpoint.private.ControlService.Status:output_type -> namespacelabs.breakpoint.private.StatusResponse
	6, // [6:9] is the sub-list for method output_type
	3, // [3:6] is the sub-list for method input_type
	3, // [3:3] is the sub-list for extension type_name
	3, // [3:3] is the sub-list for extension extendee
	0, // [0:3] is the sub-list for field type_name
}

func init() { file_api_private_v1_service_proto_init() }
func file_api_private_v1_service_proto_init() {
	if File_api_private_v1_service_proto != nil {
		return
	}
	if !protoimpl.UnsafeEnabled {
		file_api_private_v1_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
			switch v := v.(*ExtendRequest); i {
			case 0:
				return &v.state
			case 1:
				return &v.sizeCache
			case 2:
				return &v.unknownFields
			default:
				return nil
			}
		}
		file_api_private_v1_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
			switch v := v.(*ExtendResponse); i {
			case 0:
				return &v.state
			case 1:
				return &v.sizeCache
			case 2:
				return &v.unknownFields
			default:
				return nil
			}
		}
		file_api_private_v1_service_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
			switch v := v.(*StatusResponse); i {
			case 0:
				return &v.state
			case 1:
				return &v.sizeCache
			case 2:
				return &v.unknownFields
			default:
				return nil
			}
		}
	}
	type x struct{}
	out := protoimpl.TypeBuilder{
		File: protoimpl.DescBuilder{
			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
			RawDescriptor: file_api_private_v1_service_proto_rawDesc,
			NumEnums:      0,
			NumMessages:   3,
			NumExtensions: 0,
			NumServices:   1,
		},
		GoTypes:           file_api_private_v1_service_proto_goTypes,
		DependencyIndexes: file_api_private_v1_service_proto_depIdxs,
		MessageInfos:      file_api_private_v1_service_proto_msgTypes,
	}.Build()
	File_api_private_v1_service_proto = out.File
	file_api_private_v1_service_proto_rawDesc = nil
	file_api_private_v1_service_proto_goTypes = nil
	file_api_private_v1_service_proto_depIdxs = nil
}


================================================
FILE: api/private/v1/service.proto
================================================
syntax = "proto3";

package namespacelabs.breakpoint.private;

import "google/protobuf/duration.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";

option go_package = "namespacelabs.dev/breakpoint/api/private/v1";

service ControlService {
  rpc Resume(google.protobuf.Empty) returns (google.protobuf.Empty);
  rpc Extend(ExtendRequest) returns (ExtendResponse);
  rpc Status(google.protobuf.Empty) returns (StatusResponse);
}

message ExtendRequest {
  google.protobuf.Duration wait_for = 1;
}

message ExtendResponse {
  google.protobuf.Timestamp expiration = 1;
}

message StatusResponse {
    google.protobuf.Timestamp expiration      = 1;
    string                    endpoint        = 2;
    uint32                    num_connections = 3;
}


================================================
FILE: api/private/v1/service_grpc.pb.go
================================================
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
// - protoc             (unknown)
// source: api/private/v1/service.proto

package v1

import (
	context "context"
	grpc "google.golang.org/grpc"
	codes "google.golang.org/grpc/codes"
	status "google.golang.org/grpc/status"
	emptypb "google.golang.org/protobuf/types/known/emptypb"
)

// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7

const (
	ControlService_Resume_FullMethodName = "/namespacelabs.breakpoint.private.ControlService/Resume"
	ControlService_Extend_FullMethodName = "/namespacelabs.breakpoint.private.ControlService/Extend"
	ControlService_Status_FullMethodName = "/namespacelabs.breakpoint.private.ControlService/Status"
)

// ControlServiceClient is the client API for ControlService service.
//
// 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.
type ControlServiceClient interface {
	Resume(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
	Extend(ctx context.Context, in *ExtendRequest, opts ...grpc.CallOption) (*ExtendResponse, error)
	Status(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StatusResponse, error)
}

type controlServiceClient struct {
	cc grpc.ClientConnInterface
}

func NewControlServiceClient(cc grpc.ClientConnInterface) ControlServiceClient {
	return &controlServiceClient{cc}
}

func (c *controlServiceClient) Resume(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
	out := new(emptypb.Empty)
	err := c.cc.Invoke(ctx, ControlService_Resume_FullMethodName, in, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

func (c *controlServiceClient) Extend(ctx context.Context, in *ExtendRequest, opts ...grpc.CallOption) (*ExtendResponse, error) {
	out := new(ExtendResponse)
	err := c.cc.Invoke(ctx, ControlService_Extend_FullMethodName, in, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

func (c *controlServiceClient) Status(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StatusResponse, error) {
	out := new(StatusResponse)
	err := c.cc.Invoke(ctx, ControlService_Status_FullMethodName, in, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

// ControlServiceServer is the server API for ControlService service.
// All implementations must embed UnimplementedControlServiceServer
// for forward compatibility
type ControlServiceServer interface {
	Resume(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
	Extend(context.Context, *ExtendRequest) (*ExtendResponse, error)
	Status(context.Context, *emptypb.Empty) (*StatusResponse, error)
	mustEmbedUnimplementedControlServiceServer()
}

// UnimplementedControlServiceServer must be embedded to have forward compatible implementations.
type UnimplementedControlServiceServer struct {
}

func (UnimplementedControlServiceServer) Resume(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
	return nil, status.Errorf(codes.Unimplemented, "method Resume not implemented")
}
func (UnimplementedControlServiceServer) Extend(context.Context, *ExtendRequest) (*ExtendResponse, error) {
	return nil, status.Errorf(codes.Unimplemented, "method Extend not implemented")
}
func (UnimplementedControlServiceServer) Status(context.Context, *emptypb.Empty) (*StatusResponse, error) {
	return nil, status.Errorf(codes.Unimplemented, "method Status not implemented")
}
func (UnimplementedControlServiceServer) mustEmbedUnimplementedControlServiceServer() {}

// UnsafeControlServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ControlServiceServer will
// result in compilation errors.
type UnsafeControlServiceServer interface {
	mustEmbedUnimplementedControlServiceServer()
}

func RegisterControlServiceServer(s grpc.ServiceRegistrar, srv ControlServiceServer) {
	s.RegisterService(&ControlService_ServiceDesc, srv)
}

func _ControlService_Resume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
	in := new(emptypb.Empty)
	if err := dec(in); err != nil {
		return nil, err
	}
	if interceptor == nil {
		return srv.(ControlServiceServer).Resume(ctx, in)
	}
	info := &grpc.UnaryServerInfo{
		Server:     srv,
		FullMethod: ControlService_Resume_FullMethodName,
	}
	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
		return srv.(ControlServiceServer).Resume(ctx, req.(*emptypb.Empty))
	}
	return interceptor(ctx, in, info, handler)
}

func _ControlService_Extend_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
	in := new(ExtendRequest)
	if err := dec(in); err != nil {
		return nil, err
	}
	if interceptor == nil {
		return srv.(ControlServiceServer).Extend(ctx, in)
	}
	info := &grpc.UnaryServerInfo{
		Server:     srv,
		FullMethod: ControlService_Extend_FullMethodName,
	}
	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
		return srv.(ControlServiceServer).Extend(ctx, req.(*ExtendRequest))
	}
	return interceptor(ctx, in, info, handler)
}

func _ControlService_Status_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
	in := new(emptypb.Empty)
	if err := dec(in); err != nil {
		return nil, err
	}
	if interceptor == nil {
		return srv.(ControlServiceServer).Status(ctx, in)
	}
	info := &grpc.UnaryServerInfo{
		Server:     srv,
		FullMethod: ControlService_Status_FullMethodName,
	}
	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
		return srv.(ControlServiceServer).Status(ctx, req.(*emptypb.Empty))
	}
	return interceptor(ctx, in, info, handler)
}

// ControlService_ServiceDesc is the grpc.ServiceDesc for ControlService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var ControlService_ServiceDesc = grpc.ServiceDesc{
	ServiceName: "namespacelabs.breakpoint.private.ControlService",
	HandlerType: (*ControlServiceServer)(nil),
	Methods: []grpc.MethodDesc{
		{
			MethodName: "Resume",
			Handler:    _ControlService_Resume_Handler,
		},
		{
			MethodName: "Extend",
			Handler:    _ControlService_Extend_Handler,
		},
		{
			MethodName: "Status",
			Handler:    _ControlService_Status_Handler,
		},
	},
	Streams:  []grpc.StreamDesc{},
	Metadata: "api/private/v1/service.proto",
}


================================================
FILE: api/public/v1/service.pb.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// 	protoc-gen-go v1.29.1
// 	protoc        (unknown)
// source: api/public/v1/service.proto

package v1

import (
	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
	reflect "reflect"
	sync "sync"
)

const (
	// Verify that this generated code is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
	// Verify that runtime/protoimpl is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)

type RegisterRequest struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields
}

func (x *RegisterRequest) Reset() {
	*x = RegisterRequest{}
	if protoimpl.UnsafeEnabled {
		mi := &file_api_public_v1_service_proto_msgTypes[0]
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		ms.StoreMessageInfo(mi)
	}
}

func (x *RegisterRequest) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*RegisterRequest) ProtoMessage() {}

func (x *RegisterRequest) ProtoReflect() protoreflect.Message {
	mi := &file_api_public_v1_service_proto_msgTypes[0]
	if protoimpl.UnsafeEnabled && x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use RegisterRequest.ProtoReflect.Descriptor instead.
func (*RegisterRequest) Descriptor() ([]byte, []int) {
	return file_api_public_v1_service_proto_rawDescGZIP(), []int{0}
}

type RegisterResponse struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	Endpoint string `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"` // Connection endpoint, e.g. <address>:<port>
}

func (x *RegisterResponse) Reset() {
	*x = RegisterResponse{}
	if protoimpl.UnsafeEnabled {
		mi := &file_api_public_v1_service_proto_msgTypes[1]
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		ms.StoreMessageInfo(mi)
	}
}

func (x *RegisterResponse) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*RegisterResponse) ProtoMessage() {}

func (x *RegisterResponse) ProtoReflect() protoreflect.Message {
	mi := &file_api_public_v1_service_proto_msgTypes[1]
	if protoimpl.UnsafeEnabled && x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use RegisterResponse.ProtoReflect.Descriptor instead.
func (*RegisterResponse) Descriptor() ([]byte, []int) {
	return file_api_public_v1_service_proto_rawDescGZIP(), []int{1}
}

func (x *RegisterResponse) GetEndpoint() string {
	if x != nil {
		return x.Endpoint
	}
	return ""
}

var File_api_public_v1_service_proto protoreflect.FileDescriptor

var file_api_public_v1_service_proto_rawDesc = []byte{
	0x0a, 0x1b, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2f, 0x76, 0x31, 0x2f,
	0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x18, 0x6e,
	0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x6c, 0x61, 0x62, 0x73, 0x2e, 0x62, 0x72, 0x65,
	0x61, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x11, 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x73,
	0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2e, 0x0a, 0x10, 0x52, 0x65,
	0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a,
	0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
	0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x32, 0x73, 0x0a, 0x0c, 0x50, 0x72,
	0x6f, 0x78, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x08, 0x52, 0x65,
	0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x29, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61,
	0x63, 0x65, 0x6c, 0x61, 0x62, 0x73, 0x2e, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x70, 0x6f, 0x69, 0x6e,
	0x74, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
	0x74, 0x1a, 0x2a, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x6c, 0x61, 0x62,
	0x73, 0x2e, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x67,
	0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42,
	0x2c, 0x5a, 0x2a, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x6c, 0x61, 0x62, 0x73,
	0x2e, 0x64, 0x65, 0x76, 0x2f, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2f,
	0x61, 0x70, 0x69, 0x2f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70,
	0x72, 0x6f, 0x74, 0x6f, 0x33,
}

var (
	file_api_public_v1_service_proto_rawDescOnce sync.Once
	file_api_public_v1_service_proto_rawDescData = file_api_public_v1_service_proto_rawDesc
)

func file_api_public_v1_service_proto_rawDescGZIP() []byte {
	file_api_public_v1_service_proto_rawDescOnce.Do(func() {
		file_api_public_v1_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_public_v1_service_proto_rawDescData)
	})
	return file_api_public_v1_service_proto_rawDescData
}

var file_api_public_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_api_public_v1_service_proto_goTypes = []interface{}{
	(*RegisterRequest)(nil),  // 0: namespacelabs.breakpoint.RegisterRequest
	(*RegisterResponse)(nil), // 1: namespacelabs.breakpoint.RegisterResponse
}
var file_api_public_v1_service_proto_depIdxs = []int32{
	0, // 0: namespacelabs.breakpoint.ProxyService.Register:input_type -> namespacelabs.breakpoint.RegisterRequest
	1, // 1: namespacelabs.breakpoint.ProxyService.Register:output_type -> namespacelabs.breakpoint.RegisterResponse
	1, // [1:2] is the sub-list for method output_type
	0, // [0:1] is the sub-list for method input_type
	0, // [0:0] is the sub-list for extension type_name
	0, // [0:0] is the sub-list for extension extendee
	0, // [0:0] is the sub-list for field type_name
}

func init() { file_api_public_v1_service_proto_init() }
func file_api_public_v1_service_proto_init() {
	if File_api_public_v1_service_proto != nil {
		return
	}
	if !protoimpl.UnsafeEnabled {
		file_api_public_v1_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
			switch v := v.(*RegisterRequest); i {
			case 0:
				return &v.state
			case 1:
				return &v.sizeCache
			case 2:
				return &v.unknownFields
			default:
				return nil
			}
		}
		file_api_public_v1_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
			switch v := v.(*RegisterResponse); i {
			case 0:
				return &v.state
			case 1:
				return &v.sizeCache
			case 2:
				return &v.unknownFields
			default:
				return nil
			}
		}
	}
	type x struct{}
	out := protoimpl.TypeBuilder{
		File: protoimpl.DescBuilder{
			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
			RawDescriptor: file_api_public_v1_service_proto_rawDesc,
			NumEnums:      0,
			NumMessages:   2,
			NumExtensions: 0,
			NumServices:   1,
		},
		GoTypes:           file_api_public_v1_service_proto_goTypes,
		DependencyIndexes: file_api_public_v1_service_proto_depIdxs,
		MessageInfos:      file_api_public_v1_service_proto_msgTypes,
	}.Build()
	File_api_public_v1_service_proto = out.File
	file_api_public_v1_service_proto_rawDesc = nil
	file_api_public_v1_service_proto_goTypes = nil
	file_api_public_v1_service_proto_depIdxs = nil
}


================================================
FILE: api/public/v1/service.proto
================================================
syntax = "proto3";

package namespacelabs.breakpoint;

option go_package = "namespacelabs.dev/breakpoint/api/public/v1";

service ProxyService {
  // The reverse tunnel is active for as long as this stream over a quic connection is active.
  rpc Register(RegisterRequest) returns (stream RegisterResponse);
}

message RegisterRequest {}

message RegisterResponse {
  string endpoint = 1; // Connection endpoint, e.g. <address>:<port>
}


================================================
FILE: api/public/v1/service_grpc.pb.go
================================================
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
// - protoc             (unknown)
// source: api/public/v1/service.proto

package v1

import (
	context "context"
	grpc "google.golang.org/grpc"
	codes "google.golang.org/grpc/codes"
	status "google.golang.org/grpc/status"
)

// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7

const (
	ProxyService_Register_FullMethodName = "/namespacelabs.breakpoint.ProxyService/Register"
)

// ProxyServiceClient is the client API for ProxyService service.
//
// 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.
type ProxyServiceClient interface {
	// The reverse tunnel is active for as long as this stream over a quic connection is active.
	Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (ProxyService_RegisterClient, error)
}

type proxyServiceClient struct {
	cc grpc.ClientConnInterface
}

func NewProxyServiceClient(cc grpc.ClientConnInterface) ProxyServiceClient {
	return &proxyServiceClient{cc}
}

func (c *proxyServiceClient) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (ProxyService_RegisterClient, error) {
	stream, err := c.cc.NewStream(ctx, &ProxyService_ServiceDesc.Streams[0], ProxyService_Register_FullMethodName, opts...)
	if err != nil {
		return nil, err
	}
	x := &proxyServiceRegisterClient{stream}
	if err := x.ClientStream.SendMsg(in); err != nil {
		return nil, err
	}
	if err := x.ClientStream.CloseSend(); err != nil {
		return nil, err
	}
	return x, nil
}

type ProxyService_RegisterClient interface {
	Recv() (*RegisterResponse, error)
	grpc.ClientStream
}

type proxyServiceRegisterClient struct {
	grpc.ClientStream
}

func (x *proxyServiceRegisterClient) Recv() (*RegisterResponse, error) {
	m := new(RegisterResponse)
	if err := x.ClientStream.RecvMsg(m); err != nil {
		return nil, err
	}
	return m, nil
}

// ProxyServiceServer is the server API for ProxyService service.
// All implementations must embed UnimplementedProxyServiceServer
// for forward compatibility
type ProxyServiceServer interface {
	// The reverse tunnel is active for as long as this stream over a quic connection is active.
	Register(*RegisterRequest, ProxyService_RegisterServer) error
	mustEmbedUnimplementedProxyServiceServer()
}

// UnimplementedProxyServiceServer must be embedded to have forward compatible implementations.
type UnimplementedProxyServiceServer struct {
}

func (UnimplementedProxyServiceServer) Register(*RegisterRequest, ProxyService_RegisterServer) error {
	return status.Errorf(codes.Unimplemented, "method Register not implemented")
}
func (UnimplementedProxyServiceServer) mustEmbedUnimplementedProxyServiceServer() {}

// UnsafeProxyServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ProxyServiceServer will
// result in compilation errors.
type UnsafeProxyServiceServer interface {
	mustEmbedUnimplementedProxyServiceServer()
}

func RegisterProxyServiceServer(s grpc.ServiceRegistrar, srv ProxyServiceServer) {
	s.RegisterService(&ProxyService_ServiceDesc, srv)
}

func _ProxyService_Register_Handler(srv interface{}, stream grpc.ServerStream) error {
	m := new(RegisterRequest)
	if err := stream.RecvMsg(m); err != nil {
		return err
	}
	return srv.(ProxyServiceServer).Register(m, &proxyServiceRegisterServer{stream})
}

type ProxyService_RegisterServer interface {
	Send(*RegisterResponse) error
	grpc.ServerStream
}

type proxyServiceRegisterServer struct {
	grpc.ServerStream
}

func (x *proxyServiceRegisterServer) Send(m *RegisterResponse) error {
	return x.ServerStream.SendMsg(m)
}

// ProxyService_ServiceDesc is the grpc.ServiceDesc for ProxyService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var ProxyService_ServiceDesc = grpc.ServiceDesc{
	ServiceName: "namespacelabs.breakpoint.ProxyService",
	HandlerType: (*ProxyServiceServer)(nil),
	Methods:     []grpc.MethodDesc{},
	Streams: []grpc.StreamDesc{
		{
			StreamName:    "Register",
			Handler:       _ProxyService_Register_Handler,
			ServerStreams: true,
		},
	},
	Metadata: "api/public/v1/service.proto",
}


================================================
FILE: api/public/v1/types.go
================================================
package v1

const (
	QuicProto = "breakpoint-grpc"

	GitHubOIDCTokenHeader = "x-breakpoint-github-oidc-token"

	GitHubOIDCAudience = "namespacelabs.dev/breakpoint"
)


================================================
FILE: buf.gen.yaml
================================================
version: v1
plugins:
  - plugin: go
    out: .
    opt: paths=source_relative
  - plugin: go-grpc
    out: .
    opt: paths=source_relative


================================================
FILE: cmd/breakpoint/attach.go
================================================
package main

import (
	"errors"
	"net"

	"github.com/rs/zerolog"
	"github.com/spf13/cobra"
	"inet.af/tcpproxy"
	"namespacelabs.dev/breakpoint/pkg/quicproxyclient"
)

func newAttachCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use: "attach",
	}

	endpoint := cmd.Flags().String("endpoint", "", "The address of the server.")
	target := cmd.Flags().String("target", "", "Where to connect to.")

	cmd.RunE = func(cmd *cobra.Command, args []string) error {
		if *endpoint == "" {
			return errors.New("--endpoint is required")
		}

		if *target == "" {
			return errors.New("--target is required")
		}

		return quicproxyclient.Serve(cmd.Context(), *endpoint, nil, quicproxyclient.Handlers{
			OnAllocation: func(endpoint string) {
				zerolog.Ctx(cmd.Context()).Info().Str("endpoint", endpoint).Msg("Got allocation")
			},
			Proxy: func(conn net.Conn) error {
				zerolog.Ctx(cmd.Context()).Info().Str("target", *target).Msg("handling reverse proxy")
				go tcpproxy.To(*target).HandleConn(conn)
				return nil
			},
		})
	}

	return cmd
}

func init() {
	rootCmd.AddCommand(newAttachCmd())
}


================================================
FILE: cmd/breakpoint/extend.go
================================================
package main

import (
	"fmt"
	"time"

	"github.com/spf13/cobra"
	"google.golang.org/protobuf/types/known/durationpb"
	pb "namespacelabs.dev/breakpoint/api/private/v1"
	"namespacelabs.dev/breakpoint/pkg/bcontrol"
	"namespacelabs.dev/breakpoint/pkg/waiter"

	"github.com/dustin/go-humanize"
)

func newExtendCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "extend",
		Short: "Extend the breakpoint duration.",
	}

	extendWaitFor := cmd.Flags().Duration("for", time.Minute*30, "How much to extend the breakpoint by.")
	extendWaitDuration := cmd.Flags().Duration("duration", 0, "Alias of --for")
	cmd.MarkFlagsMutuallyExclusive("duration", "for")

	cmd.RunE = func(cmd *cobra.Command, args []string) error {
		duration := *extendWaitDuration
		if *extendWaitDuration == 0 {
			duration = *extendWaitFor
		}

		if duration <= 0 {
			return fmt.Errorf("duration must be positive")
		}

		clt, conn, err := bcontrol.Connect(cmd.Context())
		if err != nil {
			return err
		}

		defer conn.Close()

		resp, err := clt.Extend(cmd.Context(), &pb.ExtendRequest{
			WaitFor: durationpb.New(duration),
		})
		if err != nil {
			return err
		}

		expiration := resp.Expiration.AsTime()
		fmt.Printf("Breakpoint now expires at %s (%s)\n",
			expiration.Format(waiter.Stamp),
			humanize.Time(expiration))

		return nil
	}

	return cmd
}

func init() {
	rootCmd.AddCommand(newExtendCmd())
}


================================================
FILE: cmd/breakpoint/hold.go
================================================
package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/dustin/go-humanize"
	"github.com/spf13/cobra"
	"google.golang.org/protobuf/types/known/durationpb"
	"google.golang.org/protobuf/types/known/emptypb"
	v1 "namespacelabs.dev/breakpoint/api/private/v1"
	"namespacelabs.dev/breakpoint/pkg/bcontrol"
	"namespacelabs.dev/breakpoint/pkg/waiter"
)

func init() {
	rootCmd.AddCommand(newHoldCmd())
}

const (
	extendBy = 30 * time.Second
)

func newHoldCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "hold",
		Short: "Holds until a breakpoint is finished or for a certain amount of time.",
	}

	holdFor := cmd.Flags().Duration("for", time.Minute*30, "How much to extend the breakpoint by.")
	holdDuration := cmd.Flags().Duration("duration", 0, "Alias of --for")
	shouldHoldWhileConnected := cmd.Flags().Bool("while-connected", false, "Keep holding while there are active connections, even after duration has passed")
	stopWhenDone := cmd.Flags().Bool("stop", false, "Stop the breakpoint server after holding")
	cmd.MarkFlagsMutuallyExclusive("duration", "for", "while-connected")

	cmd.RunE = func(cmd *cobra.Command, args []string) error {
		duration := *holdDuration
		if *holdDuration == 0 {
			duration = *holdFor
		}

		ctx := cmd.Context()
		if *shouldHoldWhileConnected {
			if err := holdWhileConnected(ctx); err != nil {
				return err
			}
		} else {
			if err := holdForDuration(ctx, duration); err != nil {
				return err
			}
		}

		if *stopWhenDone {
			if err := stopBreakpoint(ctx); err != nil {
				fmt.Printf("Failed to stop breakpoint: %v\n", err)
			} else {
				fmt.Printf("Stopped breakpoint\n")
			}
		}

		return nil
	}

	return cmd
}

func holdForDuration(ctx context.Context, duration time.Duration) error {
	if duration <= 0 {
		return fmt.Errorf("duration must be positive")
	}

	status, err := getStatus(ctx)
	if err != nil {
		return err
	}
	waiter.PrintConnectionInfo(status.Endpoint, status.Expiration.AsTime(), os.Stderr)

	fmt.Printf("Holding until %s\n", humanize.Time(time.Now().Add(duration)))

	timer := time.NewTimer(duration)

	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-timer.C:
		return nil
	}
}

func holdWhileConnected(ctx context.Context) error {
	clt, conn, err := bcontrol.Connect(ctx)
	if err != nil {
		return err
	}

	defer conn.Close()

	status, err := clt.Status(ctx, &emptypb.Empty{})
	if err != nil {
		return fmt.Errorf("unable to fetch breakpoint status, is breakpoint running")
	}

	if status.GetNumConnections() < 1 {
		fmt.Printf("No active connections, exiting\n")
		return nil
	}

	waiter.PrintConnectionInfo(status.Endpoint, status.Expiration.AsTime(), os.Stderr)

	tickDuration := 5 * time.Second
	ticker := time.NewTicker(tickDuration)
	defer ticker.Stop()

	fmt.Printf("Waiting until breakpoint has no active connections\n")

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()

		case <-ticker.C:
			status, err := clt.Status(ctx, &emptypb.Empty{})
			if err != nil {
				return fmt.Errorf("unable to fetch breakpoint status, assuming no longer running")
			}

			expiration := status.GetExpiration().AsTime()
			if !expiration.IsZero() && time.Now().Add(2*tickDuration).After(expiration) {
				tryExtendBreakpoint(ctx, expiration, clt)
			}

			if status.GetNumConnections() > 0 {
				fmt.Printf("Active connections: %d, waiting\n", status.GetNumConnections())
				continue
			}

			fmt.Printf("No active connections, exiting\n")
			return nil
		}
	}
}

func tryExtendBreakpoint(ctx context.Context, currentExpiration time.Time, clt v1.ControlServiceClient) {
	fmt.Printf("Breakpoint expiring %s, extending by %s\n", humanize.Time(currentExpiration), extendBy)

	ret, err := clt.Extend(ctx, &v1.ExtendRequest{
		WaitFor: durationpb.New(extendBy),
	})
	if err != nil {
		fmt.Printf("Unable to extend breakpoint: %v\n", err)
	}

	fmt.Printf("Breakpoint now expires %s\n", humanize.Time(ret.GetExpiration().AsTime()))
}

func stopBreakpoint(ctx context.Context) error {
	clt, conn, err := bcontrol.Connect(ctx)
	if err != nil {
		return err
	}

	defer conn.Close()

	_, err = clt.Resume(ctx, &emptypb.Empty{})
	return err
}


================================================
FILE: cmd/breakpoint/main.go
================================================
package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"syscall"

	"github.com/spf13/cobra"
	"namespacelabs.dev/breakpoint/pkg/blog"
)

var rootCmd = &cobra.Command{
	Use:   "breakpoint",
	Short: `Add breakpoints to CI workflows.`,
}

func main() {
	// This is the only control we have available.
	os.Setenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING", "true")

	l := blog.New()

	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	err := rootCmd.ExecuteContext(l.WithContext(ctx))
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}


================================================
FILE: cmd/breakpoint/resume.go
================================================
package main

import (
	"fmt"

	"github.com/spf13/cobra"
	"google.golang.org/protobuf/types/known/emptypb"
	"namespacelabs.dev/breakpoint/pkg/bcontrol"
)

func newResumeCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "resume",
		Short: "Resume the workflow execution.",
	}

	cmd.RunE = func(cmd *cobra.Command, args []string) error {
		clt, conn, err := bcontrol.Connect(cmd.Context())
		if err != nil {
			return err
		}

		defer conn.Close()

		if _, err := clt.Resume(cmd.Context(), &emptypb.Empty{}); err != nil {
			return err
		}

		fmt.Printf("Breakpoint removed, workflow resuming!\n")
		return nil
	}

	return cmd
}

func init() {
	rootCmd.AddCommand(newResumeCmd())
}


================================================
FILE: cmd/breakpoint/start.go
================================================
package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"time"

	"github.com/spf13/cobra"
	"google.golang.org/protobuf/types/known/emptypb"
	v1 "namespacelabs.dev/breakpoint/api/private/v1"
	"namespacelabs.dev/breakpoint/pkg/bcontrol"
	"namespacelabs.dev/breakpoint/pkg/execbackground"
	"namespacelabs.dev/breakpoint/pkg/waiter"
)

func init() {
	rootCmd.AddCommand(newStartCmd())
}

func newStartCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "start",
		Short: "Starts breakpoint in the background",
	}

	configPath := cmd.Flags().String("config", "", "Path to the configuration file.")

	cmd.RunE = func(cmd *cobra.Command, args []string) error {
		if *configPath == "" {
			return errors.New("--config is required")
		}

		procArgs := []string{"wait", "--config", *configPath}
		proc := exec.Command(os.Args[0], procArgs...)
		execbackground.SetCreateSession(proc)

		if err := proc.Start(); err != nil {
			return fmt.Errorf("failed to start background process: %w", err)
		}

		pid := proc.Process.Pid

		fmt.Fprintf(os.Stderr, "Breakpoint starting in background (PID: %d)\n", pid)

		status, err := waitForReady(cmd.Context(), 5*time.Second)
		if err != nil {
			_ = proc.Process.Kill()
			return err
		}

		if err := proc.Process.Release(); err != nil {
			return err
		}

		waiter.PrintConnectionInfo(status.Endpoint, status.GetExpiration().AsTime(), os.Stderr)

		return nil
	}

	return cmd
}

func waitForReady(ctx context.Context, timeoutDuration time.Duration) (*v1.StatusResponse, error) {
	// Check for file existence with timeout
	timeout := time.After(timeoutDuration)
	ticker := time.NewTicker(100 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return nil, ctx.Err()

		case <-timeout:
			return nil, fmt.Errorf("breakpoint didn't start in time")

		case <-ticker.C:
			status, err := getStatus(ctx)
			if err != nil {
				continue
			}

			if status.GetEndpoint() != "" {
				return status, nil
			}
		}
	}
}

func getStatus(ctx context.Context) (*v1.StatusResponse, error) {
	clt, conn, err := bcontrol.Connect(ctx)
	if err != nil {
		return nil, err
	}

	defer conn.Close()

	status, err := clt.Status(ctx, &emptypb.Empty{})
	if err != nil {
		return nil, err
	}

	return status, nil
}


================================================
FILE: cmd/breakpoint/status.go
================================================
package main

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
	"google.golang.org/protobuf/types/known/emptypb"
	"namespacelabs.dev/breakpoint/pkg/bcontrol"
	"namespacelabs.dev/breakpoint/pkg/waiter"
)

func init() {
	rootCmd.AddCommand(newStatusCmd())
}

func newStatusCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "status",
		Short: "Get the current status of breakpoint",
	}

	cmd.RunE = func(cmd *cobra.Command, args []string) error {
		clt, conn, err := bcontrol.Connect(cmd.Context())
		if err != nil {
			fmt.Fprintln(os.Stderr, err)
			fmt.Fprintln(os.Stdout, "Unable to connect to breakpoint control server, is breakpoint running?")
			os.Exit(1)
			return nil
		}

		defer conn.Close()

		status, err := clt.Status(cmd.Context(), &emptypb.Empty{})
		if err != nil {
			fmt.Fprintln(os.Stderr, err)
			fmt.Fprintln(os.Stdout, "Unable to retrieve status from breakpoint control server, is breakpoint running?")
			os.Exit(1)
			return nil
		}

		waiter.PrintConnectionInfo(status.Endpoint, status.Expiration.AsTime(), os.Stdout)

		fmt.Fprintf(os.Stdout, "\nActive connections: %d\n", status.GetNumConnections())

		return nil
	}

	return cmd
}


================================================
FILE: cmd/breakpoint/wait.go
================================================
package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"

	"github.com/dustin/go-humanize"
	"github.com/muesli/reflow/wordwrap"
	"github.com/spf13/cobra"
	"golang.org/x/sync/errgroup"
	"namespacelabs.dev/breakpoint/pkg/config"
	"namespacelabs.dev/breakpoint/pkg/internalserver"
	"namespacelabs.dev/breakpoint/pkg/passthrough"
	"namespacelabs.dev/breakpoint/pkg/quicproxyclient"
	"namespacelabs.dev/breakpoint/pkg/sshd"
	"namespacelabs.dev/breakpoint/pkg/waiter"
)

func init() {
	rootCmd.AddCommand(newWaitCmd())
}

func newWaitCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "wait",
		Short: "Blocks for the duration of the breakpoint",
	}

	configPath := cmd.Flags().String("config", "", "Path to the configuration file.")

	cmd.RunE = func(cmd *cobra.Command, args []string) error {
		if *configPath == "" {
			return errors.New("--config is required")
		}

		ctx := cmd.Context()

		cfg, err := config.LoadConfig(ctx, *configPath)
		if err != nil {
			return err
		}

		mopts := waiter.ManagerOpts{
			InitialDur: cfg.ParsedDuration,
			Webhooks:   cfg.Webhooks,
		}

		if cfg.SlackBot != nil {
			mopts.SlackBots = append(mopts.SlackBots, *cfg.SlackBot)
		}

		mgr, ctx := waiter.NewManager(ctx, mopts)

		sshd, err := sshd.MakeServer(ctx, sshd.SSHServerOpts{
			Shell:          cfg.Shell,
			AuthorizedKeys: cfg.AllKeys,
			AllowedUsers:   cfg.AllowedSSHUsers,
			Env:            os.Environ(),
			InteractiveMOTD: func(w io.Writer) {
				ww := wordwrap.NewWriter(80)

				fmt.Fprintln(ww)
				fmt.Fprintf(ww, "Welcome to a breakpoint-provided remote shell.\n")
				fmt.Fprintln(ww)
				fmt.Fprintf(ww, "This breakpoint will expire %s.\n", humanize.Time(mgr.Expiration()))
				fmt.Fprintln(ww)
				fmt.Fprintf(ww, "The following additional commands are available:\n\n")
				fmt.Fprintf(ww, " - `breakpoint extend` to extend the breakpoint duration.\n")
				fmt.Fprintf(ww, " - `breakpoint resume` to resume immediately.\n")

				_ = ww.Close()

				_, _ = w.Write(ww.Bytes())
			},
		})
		if err != nil {
			return err
		}

		mgr.SetConnectionCountCallback(sshd.NumConnections)

		eg, ctx := errgroup.WithContext(ctx)

		pl := passthrough.NewListener(ctx, dummyAddr{})

		eg.Go(func() error {
			return sshd.Server.Serve(pl)
		})

		eg.Go(func() error {
			defer pl.Close()

			return quicproxyclient.Serve(ctx, cfg.Endpoint, cfg.RegisterMetadata, quicproxyclient.Handlers{
				OnAllocation: func(endpoint string) {
					mgr.SetEndpoint(endpoint)
				},
				Proxy: pl.Offer,
			})
		})

		eg.Go(func() error {
			return internalserver.ListenAndServe(ctx, mgr)
		})

		eg.Go(func() error {
			return mgr.Wait()
		})

		return cancelIsOK(eg.Wait())
	}

	return cmd
}

func cancelIsOK(err error) error {
	if errors.Is(err, context.Canceled) {
		return nil
	}

	return err
}

type dummyAddr struct{}

func (dummyAddr) Network() string { return "internal" }
func (dummyAddr) String() string  { return "quic-revproxy" }


================================================
FILE: cmd/rendezvous/main.go
================================================
package main

import (
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"log"
	"net"
	"net/http"
	"net/netip"
	"os"
	"strings"

	"golang.org/x/exp/slices"
	"golang.org/x/sync/errgroup"
	"namespacelabs.dev/breakpoint/pkg/blog"
	"namespacelabs.dev/breakpoint/pkg/quicproxy"
	"namespacelabs.dev/breakpoint/pkg/tlscerts"
)

var (
	listenOn         = flag.String("l", "", "The address:port to listen on.")
	publicAddress    = flag.String("pub", "", "If unset, defaults to listen address.")
	subjectDomains   = flag.String("sub", "", "Attaches the specified domain names as TLS cert subjects.")
	frontend         = flag.String("frontend", "", "If specified, configures the frontend (in JSON).")
	httpPort         = flag.Int("http_port", 10020, "Where we listen on HTTP.")
	enableGitHubOIDC = flag.Bool("validate_github_oidc", false, "Validate GitHub OIDC tokens.")
	redirectTarget   = flag.String("redirect_target", "https://github.com/namespacelabs/breakpoint", "Where to redirect users to when accessed via HTTP.")
)

type frontendConfig struct {
	Kind       string `json:"kind"`
	PortStart  int    `json:"port_start"`
	PortEnd    int    `json:"port_end"`
	PortListen int    `json:"listen_port"`
}

func main() {
	flag.Parse()

	var fcfg frontendConfig
	if frontendData := flagOrEnv("PROXY_FRONTEND", *frontend); frontendData != "" {
		if err := json.Unmarshal([]byte(frontendData), &fcfg); err != nil {
			log.Fatal(err)
		}
	}

	var domains []string
	if val := flagOrEnv("PROXY_DOMAINS", *subjectDomains); len(val) > 0 {
		domains = strings.Split(val, ",")
	}

	if err := run(Config{
		ListenAddr:       flagOrEnv("PROXY_LISTEN", *listenOn),
		HttpPort:         *httpPort,
		FrontendConfig:   fcfg,
		PublicAddr:       flagOrEnv("PROXY_PUBLIC", *publicAddress),
		Domains:          domains,
		EnableGitHubOIDC: flagOrEnvBool("PROXY_VALIDATE_GITHUB_OIDC", *enableGitHubOIDC),
		RedirectURL:      *redirectTarget,
	}); err != nil {
		log.Fatal(err)
	}
}

func flagOrEnv(env, flag string) string {
	if flag != "" {
		return flag
	}

	return os.Getenv(env)
}

func flagOrEnvBool(env string, flag bool) bool {
	return flag || os.Getenv(env) == "true" || os.Getenv(env) == "1"
}

type Config struct {
	ListenAddr       string
	HttpPort         int
	FrontendConfig   frontendConfig
	PublicAddr       string
	Domains          []string
	EnableGitHubOIDC bool
	RedirectURL      string
}

func run(opts Config) error {
	if opts.ListenAddr == "" {
		return errors.New("-l or PROXY_LISTEN is required")
	}

	if opts.PublicAddr == "" {
		addrport, err := netip.ParseAddrPort(opts.ListenAddr)
		if err != nil {
			return err
		}

		opts.PublicAddr = addrport.Addr().String()
	}

	subjects := tlscerts.Subjects{
		DNSNames: opts.Domains,
	}

	if addr, err := netip.ParseAddr(opts.PublicAddr); err == nil {
		if !addr.IsUnspecified() {
			subjects.IPAddresses = append(subjects.IPAddresses, net.IP(addr.AsSlice()))
		}
	} else {
		if !slices.Contains(subjects.DNSNames, opts.PublicAddr) {
			subjects.DNSNames = append(subjects.DNSNames, opts.PublicAddr)
		}
	}

	frontend := makeFrontend(opts.FrontendConfig, opts.PublicAddr)

	l := blog.New()
	ctx := l.WithContext(context.Background())

	proxy, err := quicproxy.NewServer(ctx, quicproxy.ServerOpts{
		ProxyFrontend:    frontend,
		ListenAddr:       opts.ListenAddr,
		Subjects:         subjects,
		EnableGitHubOIDC: opts.EnableGitHubOIDC,
	})
	if err != nil {
		return err
	}

	eg, ctx := errgroup.WithContext(ctx)
	eg.Go(func() error {
		return frontend.ListenAndServe(ctx)
	})

	eg.Go(func() error {
		return proxy.Serve(ctx)
	})

	eg.Go(func() error {
		h := http.NewServeMux()

		h.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Location", opts.RedirectURL)
			w.WriteHeader(http.StatusTemporaryRedirect)
			fmt.Fprintf(w, "Heading over to <a href=%q>%s</a>", opts.RedirectURL, opts.RedirectURL)
		}))

		return http.ListenAndServe(fmt.Sprintf(":%d", opts.HttpPort), h)
	})

	return eg.Wait()
}

func makeFrontend(fcfg frontendConfig, pub string) quicproxy.ProxyFrontend {
	switch fcfg.Kind {
	case "proxy_proto":
		return &quicproxy.ProxyProtoFrontend{
			ListenPort: fcfg.PortListen,
			PortStart:  fcfg.PortStart,
			PortEnd:    fcfg.PortEnd,
			PublicAddr: pub,
		}

	default:
		return quicproxy.RawFrontend{
			PublicAddr: pub,
		}
	}
}


================================================
FILE: docs/CONTRIBUTING.md
================================================
# Contributing to Breakpoint

## Where to Start

You 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).

## Issues tracking and Pull Requests

We don't enforce any rigid contributing procedure. We appreciate you spending time improving `breakpoint`!

If 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.

Please include relevant GitHub Issues in the PR message when opening a Pull Request.

## Development

Developing `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.

Follow the instructions to install them for your operating system:

- [Install nix](https://github.com/DeterminateSystems/nix-installer)
- Docker: [Docker engine](https://docs.docker.com/engine/install/) or [OrbStack](https://docs.docker.com/engine/install/)

When `nix` is installed, you can:

- Run `nix develop` to enter a shell with every dependency pre-setup (e.g. Go, `buf`, etc.)
- Use the "nix environment selector" VSCode extension to apply a nix environment in VSCode.

### Building

Compiling the Go binaries:

```bash
$ go build -o . ./cmd/...

# Binaries available in the current working directory

$ ls breakpoint; ls rendezvous;
```

Installing the Go binaries:

```bash
$ go install ./cmd/...

# Binaries installed in $GOPATH

$ which breakpoint; which rendezvous;
```

Building the Docker image of Rendezvous server:

```bash
$ docker build . -t rendezvous:latest
```

### Protos

Breakpoint 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.

Whenever you change the protos definition under the [`api/`](../api) folder, then you must also regenerate the Go code:

```bash
$ buf generate
```

This will add changes to the Go files under the [`api/`](../api) folder. Include them in your commit.


================================================
FILE: docs/server-setup.md
================================================
# Rendezvous Server Setup

The `rendezvous` source code is 100% open-source and you can self-host it wherever you want.

## Requirements

Rendezvous Server needs two main properties in order to function:

1. Public IP
2. The process can listen to any port
3. Traffic to all ports is allowed in both directions (ingress and egress)

## Fly.io Deployment

Breakpoint provides a ready-to-deploy Fly.io configuration.

Create a Fly.io application.

```bash
$ flyctl apps create rendezvous
```

Allocate a public IPv4 address and assign it to the application. Note that this is a paid feature of Fly.io.

```bash
$ flyctl ips allocate-v4 -a rendezvous
```

Take note of the public IPv4 address created before and deploy the `rendezvous` service.

```bash
$ flyctl deploy -a rendezvous --env PROXY_PUBLIC={public_ip}
```

Done! Now your instance of Rendezvous Server is listening to `{public_ip}:5000` endpoint.


================================================
FILE: examples/wait.withslack.json
================================================
{
  "webhooks": [
    {
      "url": "${SLACK_WEBHOOK_URL}",
      "payload": {
        "blocks": [
          {
            "type": "header",
            "text": {
              "type": "plain_text",
              "text": "Workflow failed",
              "emoji": true
            }
          },
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "*Repository:* <https://github.com/${GITHUB_REPOSITORY}/tree/${GITHUB_REF_NAME}|${GITHUB_REPOSITORY}> (${GITHUB_REF_NAME})"
            }
          },
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "*Workflow:* ${GITHUB_WORKFLOW} (<https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}|Run #${GITHUB_RUN_NUMBER}>)"
            }
          },
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "*SSH:* `ssh -p ${BREAKPOINT_PORT} runner@${BREAKPOINT_HOST}`"
            }
          },
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "*Expires:* in ${BREAKPOINT_TIME_LEFT} (${BREAKPOINT_EXPIRATION})"
            }
          },
          {
            "type": "context",
            "elements": [
              {
                "type": "plain_text",
                "text": "Actor: ${GITHUB_ACTOR}",
                "emoji": true
              }
            ]
          }
        ]
      }
    }
  ]
}


================================================
FILE: flake.nix
================================================
{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = {
    self,
    nixpkgs,
    flake-utils,
    ...
  }:
    flake-utils.lib.eachDefaultSystem (system: let
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      devShell = pkgs.mkShell {
        buildInputs = with pkgs;
          [
            go_1_20
            buf
            protobuf
            protoc-gen-go
            protoc-gen-go-grpc
            goreleaser
          ];
      };
    });
}



================================================
FILE: fly.toml
================================================
[build]
dockerfile = "Dockerfile"

[env]
PROXY_LISTEN = "fly-global-services:5000"
PROXY_PUBLIC = "rendezvous.namespace.so"
PROXY_FRONTEND = '{"kind": "proxy_proto", "port_start": 2000, "port_end": 60000, "listen_port": 10000}'
PROXY_VALIDATE_GITHUB_OIDC = "true"


[[services]]
internal_port = 5000
protocol = "udp"
auto_stop_machines = false
auto_start_machines = false

    [[services.ports]]
    port = "5000"

[[services]]
internal_port = 10000
protocol = "tcp"
auto_stop_machines = false
auto_start_machines = false

    [[services.ports]]
    handlers = ["proxy_proto"]
    start_port = 2000
    end_port = 60000

[[services]]
internal_port = 10020
protocol = "tcp"
auto_stop_machines = false
auto_start_machines = false

    [[services.ports]]
    handlers = ["http"]
    port = 80
    force_https = true

    [[services.ports]]
    handlers = ["tls", "http"]
    port = 443


================================================
FILE: go.mod
================================================
module namespacelabs.dev/breakpoint

go 1.20

require (
	github.com/MicahParks/keyfunc v1.9.0
	github.com/creack/pty v1.1.18
	github.com/dustin/go-humanize v1.0.1
	github.com/gliderlabs/ssh v0.3.5
	github.com/golang-jwt/jwt/v4 v4.4.2
	github.com/google/go-cmp v0.5.9
	github.com/google/go-github/v52 v52.0.0
	github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
	github.com/muesli/reflow v0.3.0
	github.com/pires/go-proxyproto v0.7.0
	github.com/pkg/sftp v1.13.5
	github.com/quic-go/quic-go v0.40.0
	github.com/rs/zerolog v1.29.1
	github.com/slack-go/slack v0.12.2
	github.com/spf13/cobra v1.7.0
	go.uber.org/atomic v1.7.0
	golang.org/x/crypto v0.7.0
	golang.org/x/exp v0.0.0-20221205204356-47842c84f3db
	golang.org/x/sync v0.2.0
	google.golang.org/grpc v1.55.0
	google.golang.org/protobuf v1.30.0
	inet.af/tcpproxy v0.0.0-20221017015627-91f861402626
)

require (
	github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
	github.com/beorn7/perks v1.0.1 // indirect
	github.com/cespare/xxhash/v2 v2.2.0 // indirect
	github.com/cloudflare/circl v1.3.3 // indirect
	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
	github.com/golang/protobuf v1.5.3 // indirect
	github.com/google/go-querystring v1.1.0 // indirect
	github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
	github.com/gorilla/websocket v1.4.2 // indirect
	github.com/inconshreveable/mousetrap v1.1.0 // indirect
	github.com/kr/fs v0.1.0 // indirect
	github.com/mattn/go-colorable v0.1.12 // indirect
	github.com/mattn/go-isatty v0.0.14 // indirect
	github.com/mattn/go-runewidth v0.0.12 // indirect
	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
	github.com/onsi/ginkgo/v2 v2.9.5 // indirect
	github.com/prometheus/client_golang v1.15.1 // indirect
	github.com/prometheus/client_model v0.3.0 // indirect
	github.com/prometheus/common v0.42.0 // indirect
	github.com/prometheus/procfs v0.9.0 // indirect
	github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
	github.com/rivo/uniseg v0.2.0 // indirect
	github.com/spf13/pflag v1.0.5 // indirect
	go.uber.org/mock v0.3.0 // indirect
	golang.org/x/mod v0.11.0 // indirect
	golang.org/x/net v0.10.0 // indirect
	golang.org/x/oauth2 v0.7.0 // indirect
	golang.org/x/sys v0.8.0 // indirect
	golang.org/x/text v0.9.0 // indirect
	golang.org/x/tools v0.9.1 // indirect
	google.golang.org/appengine v1.6.7 // indirect
	google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
)


================================================
FILE: go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v52 v52.0.0 h1:uyGWOY+jMQ8GVGSX8dkSwCzlehU3WfdxQ7GweO/JP7M=
github.com/google/go-github/v52 v52.0.0/go.mod h1:WJV6VEEUPuMo5pXqqa2ZCZEdbQqua4zAk2MZTIo+m+4=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw=
github.com/quic-go/quic-go v0.40.0/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ=
github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
inet.af/tcpproxy v0.0.0-20221017015627-91f861402626 h1:2dMP3Ox/Wh5BiItwOt4jxRsfzkgyBrHzx2nW28Yg6nc=
inet.af/tcpproxy v0.0.0-20221017015627-91f861402626/go.mod h1:Tojt5kmHpDIR2jMojxzZK2w2ZR7OILODmUo2gaSwjrk=


================================================
FILE: pkg/README.md
================================================
Main components and packages.

================================================
FILE: pkg/bcontrol/client.go
================================================
package bcontrol

import (
	"context"
	"net"
	"os"
	"path/filepath"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	pb "namespacelabs.dev/breakpoint/api/private/v1"
	"namespacelabs.dev/breakpoint/pkg/bgrpc"
)

func SocketPath() (string, error) {
	dir, err := os.UserConfigDir()
	if err != nil {
		return dir, err
	}

	return filepath.Join(dir, "breakpoint/breakpoint.sock"), nil
}

func Connect(ctx context.Context) (pb.ControlServiceClient, *grpc.ClientConn, error) {
	socketPath, err := SocketPath()
	if err != nil {
		return nil, nil, err
	}

	conn, err := bgrpc.DialContext(ctx, socketPath,
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
			var d net.Dialer
			return d.DialContext(ctx, "unix", socketPath)
		}))
	if err != nil {
		return nil, nil, err
	}

	return pb.NewControlServiceClient(conn), conn, nil
}


================================================
FILE: pkg/bgrpc/bgrpc.go
================================================
package bgrpc

import (
	"context"

	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
	grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
	"google.golang.org/grpc"
)

func DialContext(ctx context.Context, target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
	unary, streaming := clientInterceptors()

	opts = append(opts,
		grpc.WithStreamInterceptor(grpc_middleware.ChainStreamClient(streaming...)),
		grpc.WithUnaryInterceptor(grpc_middleware.ChainUnaryClient(unary...)),
	)

	return grpc.DialContext(ctx, target, opts...)
}

func clientInterceptors() ([]grpc.UnaryClientInterceptor, []grpc.StreamClientInterceptor) {
	return []grpc.UnaryClientInterceptor{
			grpc_prometheus.UnaryClientInterceptor,
		}, []grpc.StreamClientInterceptor{
			grpc_prometheus.StreamClientInterceptor,
		}
}


================================================
FILE: pkg/blog/blog.go
================================================
package blog

import (
	"os"
	"time"

	"github.com/rs/zerolog"
)

func init() {
	zerolog.TimeFieldFormat = time.RFC3339Nano
}

func New() zerolog.Logger {
	return zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339Nano}).
		With().Timestamp().Logger().Level(zerolog.InfoLevel)
}


================================================
FILE: pkg/config/config.go
================================================
package config

import (
	"context"
	"errors"
	"fmt"
	"os"
	"runtime"
	"time"

	"github.com/rs/zerolog"
	"google.golang.org/grpc/metadata"
	internalv1 "namespacelabs.dev/breakpoint/api/private/v1"
	v1 "namespacelabs.dev/breakpoint/api/public/v1"
	"namespacelabs.dev/breakpoint/pkg/github"
	"namespacelabs.dev/breakpoint/pkg/githuboidc"
	"namespacelabs.dev/breakpoint/pkg/jsonfile"
)

func LoadConfig(ctx context.Context, file string) (ParsedConfig, error) {
	var cfg ParsedConfig
	if err := jsonfile.Load(file, &cfg.WaitConfig); err != nil {
		return cfg, err
	}

	if cfg.Endpoint == "" {
		return cfg, errors.New("missing endpoint")
	}

	for _, wh := range cfg.Webhooks {
		if wh.URL == "" {
			return cfg, errors.New("webhook is missing url")
		}
	}

	if len(cfg.Shell) == 0 {
		if sh, ok := os.LookupEnv("SHELL"); ok {
			cfg.Shell = []string{sh}
		} else {
			if runtime.GOOS == "windows" {
				cfg.Shell = []string{"C:\\Windows\\System32\\cmd.exe"}
			} else {
				cfg.Shell = []string{"/bin/sh"}
			}
		}
	}

	requireGitHubOIDC := false
	for _, feature := range cfg.Enable {
		switch feature {
		case "github/oidc":
			// Force enable.
			requireGitHubOIDC = false

		default:
			return cfg, fmt.Errorf("unknown feature %q", feature)
		}
	}

	cfg.RegisterMetadata = metadata.MD{}
	if githuboidc.OIDCAvailable() || requireGitHubOIDC {
		token, err := githuboidc.JWT(ctx, v1.GitHubOIDCAudience)
		if err != nil {
			if requireGitHubOIDC {
				return cfg, err
			}

			zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to obtain GitHUB OIDC token")
		} else {
			cfg.RegisterMetadata[v1.GitHubOIDCTokenHeader] = []string{token.Value}
		}
	}

	dur, err := time.ParseDuration(cfg.Duration)
	if err != nil {
		return cfg, err
	}

	cfg.ParsedDuration = dur

	keyMap, err := github.ResolveSSHKeys(ctx, cfg.AuthorizedGithubUsers)
	if err != nil {
		return cfg, err
	}

	revIndex := map[string]string{}

	for _, key := range cfg.AuthorizedKeys {
		revIndex[key] = key
	}

	for user, keys := range keyMap {
		for _, key := range keys {
			revIndex[key] = user
		}
	}

	cfg.AllKeys = revIndex
	return cfg, nil
}

type ParsedConfig struct {
	internalv1.WaitConfig

	AllKeys          map[string]string // Key ID -> Owned name
	ParsedDuration   time.Duration
	RegisterMetadata metadata.MD
}


================================================
FILE: pkg/execbackground/bg_unix.go
================================================
//go:build !windows

package execbackground

import (
	"os/exec"
	"syscall"
)

func SetCreateSession(cmd *exec.Cmd) {
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Setsid: true,
	}
}


================================================
FILE: pkg/execbackground/bg_windows.go
================================================
//go:build windows

package execbackground

import "os/exec"

func SetCreateSession(cmd *exec.Cmd) {
	panic("not supported")
}


================================================
FILE: pkg/github/sshkeys.go
================================================
package github

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/rs/zerolog"
)

func ResolveSSHKeys(ctx context.Context, usernames []string) (map[string][]string, error) {
	// Fetch in sequence to minimize how many requests in parallel we issue to GitHub.

	m := map[string][]string{}
	for _, username := range usernames {
		t := time.Now()

		keys, err := fetchKeys(username)
		if err != nil {
			return nil, fmt.Errorf("failed to fetch SSH keys for GitHub user %q: %w", username, err)
		}

		if len(keys) == 0 {
			zerolog.Ctx(ctx).Warn().Str("username", username).Dur("took", time.Since(t)).Msg("No keys found")
			continue
		}

		m[username] = keys

		zerolog.Ctx(ctx).Info().Str("username", username).Dur("took", time.Since(t)).Msg("Resolved keys")
	}

	return m, nil
}

func fetchKeys(username string) ([]string, error) {
	resp, err := http.Get(fmt.Sprintf("https://github.com/%s.keys", username))
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
	}

	contents, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read body: %w", err)
	}

	var keys []string
	for _, line := range strings.FieldsFunc(strings.TrimSpace(string(contents)), func(r rune) bool { return r == '\n' }) {
		keys = append(keys, strings.TrimSpace(line))
	}

	return keys, nil
}


================================================
FILE: pkg/githuboidc/claims.go
================================================
package githuboidc

import "github.com/golang-jwt/jwt/v4"

type Claims struct {
	jwt.RegisteredClaims

	JobWorkflowRef    string `json:"job_workflow_ref"`
	Sha               string `json:"sha"`
	EventName         string `json:"event_name"`
	Repository        string `json:"repository"`
	Workflow          string `json:"workflow"`
	Ref               string `json:"ref"`
	JobWorkflowSha    string `json:"job_workflow_sha"`
	RunnerEnvironment string `json:"runner_environment"`
	RepositoryID      string `json:"repository_id"`
	RepositoryOwner   string `json:"repository_owner"`
	RepositoryOwnerID string `json:"repository_owner_id"`
	WorkflowRef       string `json:"workflow_ref"`
	WorkflowSha       string `json:"workflow_sha"`
	RunID             string `json:"run_id"`
	RunAttempt        string `json:"run_attempt"`
}


================================================
FILE: pkg/githuboidc/gh.go
================================================
package githuboidc

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"

	"namespacelabs.dev/breakpoint/pkg/httperrors"
)

var ErrMissingIdTokenWrite = errors.New("please add `id-token: write` to your workflow permissions")

const (
	userAgent = "actions/oidc-client"
)

type Token struct {
	Value string `json:"value"`
}

func OIDCAvailable() bool {
	x, y := oidcConf()
	return x != "" && y != ""
}

func JWT(ctx context.Context, audience string) (*Token, error) {
	idTokenURL, idToken := oidcConf()
	if idTokenURL == "" || idToken == "" {
		return nil, ErrMissingIdTokenWrite
	}

	if audience != "" {
		idTokenURL += fmt.Sprintf("&audience=%s", url.QueryEscape(audience))
	}

	req, err := http.NewRequestWithContext(ctx, "GET", idTokenURL, nil)
	if err != nil {
		return nil, fmt.Errorf("github/oidc: failed to create HTTP request: %w", err)
	}

	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Add("User-Agent", userAgent)
	req.Header.Add("Authorization", "Bearer "+idToken)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("github/oidc: failed to request github JWT: %w", err)
	}

	defer resp.Body.Close()

	if err := httperrors.MaybeError(resp); err != nil {
		return nil, fmt.Errorf("github/oidc: failed to obtain token: %v", err)
	}

	var token Token
	if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
		return nil, fmt.Errorf("github/oidc: bad response: %w", err)
	}

	return &token, nil
}

func oidcConf() (string, string) {
	idTokenURL := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL")
	idToken := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN")

	return idTokenURL, idToken
}


================================================
FILE: pkg/githuboidc/verifier.go
================================================
package githuboidc

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/MicahParks/keyfunc"
	"github.com/golang-jwt/jwt/v4"
	"github.com/rs/zerolog"
)

const (
	githubJWKSURL = "https://token.actions.githubusercontent.com/.well-known/jwks"
)

func ProvideVerifier(ctx context.Context) (*keyfunc.JWKS, error) {
	options := keyfunc.Options{
		Ctx: ctx,
		RefreshErrorHandler: func(err error) {
			zerolog.Ctx(ctx).Err(err).Str("jwks_url", githubJWKSURL).Msg("Failed to refresh JWKS")
		},
		RefreshInterval:   time.Hour,
		RefreshRateLimit:  time.Minute * 5,
		RefreshTimeout:    time.Second * 10,
		RefreshUnknownKID: true,
	}

	return keyfunc.Get(githubJWKSURL, options)
}

func Validate(ctx context.Context, jwks *keyfunc.JWKS, tokenStr string) (*Claims, error) {
	claims := &Claims{}

	token, err := jwt.ParseWithClaims(tokenStr, claims, jwks.Keyfunc)
	if err != nil {
		return nil, fmt.Errorf("failed to verify Github JWT: %w", err)
	}

	if !token.Valid {
		return nil, errors.New("invalid Github JWT")
	}

	return claims, nil
}


================================================
FILE: pkg/httperrors/httperrors.go
================================================
package httperrors

import (
	"fmt"
	"io"
	"net/http"
)

type HttpError struct {
	StatusCode  int
	ServerError string
}

func (he HttpError) Error() string {
	if len(he.ServerError) > 0 {
		return fmt.Sprintf("request failed with %s, got from the server:\n%s", http.StatusText(he.StatusCode), he.ServerError)
	}

	return fmt.Sprintf("request failed with %s", http.StatusText(he.StatusCode))
}

func MaybeError(resp *http.Response) error {
	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return &HttpError{StatusCode: resp.StatusCode, ServerError: string(bodyBytes)}
	}

	return nil
}


================================================
FILE: pkg/internalserver/internalserver.go
================================================
package internalserver

import (
	"context"
	"log"
	"net"
	"os"
	"path/filepath"

	"golang.org/x/sync/errgroup"
	"google.golang.org/grpc"
	"google.golang.org/protobuf/types/known/emptypb"
	"google.golang.org/protobuf/types/known/timestamppb"
	pb "namespacelabs.dev/breakpoint/api/private/v1"
	"namespacelabs.dev/breakpoint/pkg/bcontrol"
	"namespacelabs.dev/breakpoint/pkg/waiter"
)

type waiterService struct {
	manager *waiter.Manager
	pb.UnimplementedControlServiceServer
}

func ListenAndServe(ctx context.Context, mgr *waiter.Manager) error {
	socketPath, err := bcontrol.SocketPath()
	if err != nil {
		return err
	}

	if err := os.MkdirAll(filepath.Dir(socketPath), 0755); err != nil {
		return err
	}

	_ = os.Remove(socketPath) // Remove any leftovers.

	defer func() {
		_ = os.Remove(socketPath)
	}()

	var d net.ListenConfig
	lis, err := d.Listen(ctx, "unix", socketPath)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	grpcServer := grpc.NewServer()
	pb.RegisterControlServiceServer(grpcServer, waiterService{
		manager: mgr,
	})

	eg, ctx := errgroup.WithContext(ctx)

	eg.Go(func() error {
		<-ctx.Done()
		grpcServer.Stop()
		return nil
	})

	eg.Go(func() error {
		return grpcServer.Serve(lis)
	})

	return eg.Wait()
}

func (g waiterService) Extend(ctx context.Context, req *pb.ExtendRequest) (*pb.ExtendResponse, error) {
	expiration := g.manager.ExtendWait(req.WaitFor.AsDuration())
	return &pb.ExtendResponse{
		Expiration: timestamppb.New(expiration),
	}, nil
}

func (g waiterService) Status(ctx context.Context, req *emptypb.Empty) (*pb.StatusResponse, error) {
	status := g.manager.Status()
	return &pb.StatusResponse{
		Expiration:     timestamppb.New(status.Expiration),
		Endpoint:       status.Endpoint,
		NumConnections: status.NumConnections,
	}, nil
}

func (g waiterService) Resume(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) {
	g.manager.StopWait()
	return &emptypb.Empty{}, nil
}


================================================
FILE: pkg/jsonfile/load.go
================================================
package jsonfile

import (
	"encoding/json"
	"os"
)

func Load(filename string, target any) error {
	f, err := os.Open(filename)
	if err != nil {
		return err
	}

	return json.NewDecoder(f).Decode(target)
}


================================================
FILE: pkg/passthrough/listener.go
================================================
package passthrough

import (
	"context"
	"errors"
	"net"

	"go.uber.org/atomic"
)

type Listener struct {
	ctx    context.Context
	addr   net.Addr
	ch     chan net.Conn
	closed *atomic.Bool
}

func NewListener(ctx context.Context, addr net.Addr) Listener {
	return Listener{ctx: ctx, addr: addr, ch: make(chan net.Conn), closed: atomic.NewBool(false)}
}

func (pl Listener) Accept() (net.Conn, error) {
	select {
	case <-pl.ctx.Done():
		return nil, pl.ctx.Err()

	case conn, ok := <-pl.ch:
		if !ok {
			return nil, errors.New("listener is closed")
		}
		return conn, nil
	}
}

func (pl Listener) Addr() net.Addr {
	return pl.addr
}

func (pl Listener) Close() error {
	if !pl.closed.Swap(true) {
		close(pl.ch)
		return nil
	} else {
		return errors.New("already closed")
	}
}

func (pl Listener) Offer(conn net.Conn) error {
	if pl.closed.Load() {
		return errors.New("listener closed")
	}

	pl.ch <- conn
	return nil
}


================================================
FILE: pkg/quicgrpc/grpccreds.go
================================================
package quicgrpc

import (
	"context"
	"net"

	"github.com/quic-go/quic-go"
	"google.golang.org/grpc/credentials"
	"namespacelabs.dev/breakpoint/pkg/quicnet"
)

type QuicCreds struct {
	NonQuicCreds credentials.TransportCredentials
}

var _ credentials.TransportCredentials = QuicCreds{}

func (m QuicCreds) ClientHandshake(ctx context.Context, addr string, conn net.Conn) (net.Conn, credentials.AuthInfo, error) {
	return m.NonQuicCreds.ClientHandshake(ctx, addr, conn)
}

func (m QuicCreds) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) {
	if quic, ok := conn.(quicnet.Conn); ok {
		return conn, QuicAuthInfo{Conn: quic.Conn}, nil
	}

	return m.NonQuicCreds.ServerHandshake(conn)
}

func (m QuicCreds) Info() credentials.ProtocolInfo {
	return credentials.ProtocolInfo{SecurityProtocol: "insecure"}
}

func (m QuicCreds) Clone() credentials.TransportCredentials {
	return QuicCreds{NonQuicCreds: m.NonQuicCreds.Clone()}
}

func (m QuicCreds) OverrideServerName(string) error {
	return nil
}

type QuicAuthInfo struct {
	credentials.CommonAuthInfo
	Conn quic.Connection
}

func (QuicAuthInfo) AuthType() string {
	return "quic"
}


================================================
FILE: pkg/quicnet/conn.go
================================================
package quicnet

import (
	"context"
	"net"

	"github.com/quic-go/quic-go"
)

type Conn struct {
	quic.Stream
	Conn quic.Connection
}

func (cw Conn) LocalAddr() net.Addr {
	return cw.Conn.LocalAddr()
}

func (cw Conn) RemoteAddr() net.Addr {
	return cw.Conn.RemoteAddr()
}

func OpenStream(ctx context.Context, conn quic.Connection) (Conn, error) {
	stream, err := conn.OpenStreamSync(ctx)
	if err != nil {
		return Conn{}, err
	}

	return Conn{Stream: stream, Conn: conn}, nil

}


================================================
FILE: pkg/quicnet/listener.go
================================================
package quicnet

import (
	"context"
	"errors"
	"net"
	"sync"
	"time"

	"github.com/quic-go/quic-go"
	"github.com/rs/zerolog"
)

var (
	errClosed        = errors.New("closed")
	errAlreadyClosed = errors.New("already closed")
)

type Listener struct {
	ctx      context.Context
	listener quic.Listener

	mu    sync.Mutex
	cond  *sync.Cond
	inbox []net.Conn
	lErr  error // If set, the listener is closed.
}

func NewListener(ctx context.Context, l quic.Listener) *Listener {
	lst := &Listener{ctx: ctx, listener: l}
	lst.cond = sync.NewCond(&lst.mu)
	go lst.loop()
	return lst
}

func (l *Listener) loop() {
	for {
		conn, err := l.listener.Accept(l.ctx)
		if err != nil {
			_ = l.closeWithErr(err)
			return
		}

		go l.waitForStream(conn)
	}
}

func (l *Listener) closeWithErr(err error) error {
	l.mu.Lock()
	wasErr := l.lErr
	inbox := l.inbox
	if l.lErr == nil {
		l.lErr = err
		l.inbox = nil
	}
	l.cond.Broadcast()
	l.mu.Unlock()

	if wasErr != nil {
		return errAlreadyClosed
	}

	for _, conn := range inbox {
		_ = conn.Close()
	}

	_ = l.listener.Close()

	return nil
}

func (l *Listener) waitForStream(conn quic.Connection) {
	// If we don't see a stream within the deadline, then close the connection.
	ctx, done := context.WithTimeout(l.ctx, 10*time.Second)
	defer done()

	stream, err := conn.AcceptStream(ctx)
	if err != nil {
		zerolog.Ctx(ctx).Info().Stringer("remote_addr", conn.RemoteAddr()).
			Stringer("local_addr", conn.LocalAddr()).Err(err).Msg("Failed to accept stream")
		conn.CloseWithError(0, "")
		return
	}

	l.queue(conn, stream)
}

func (l *Listener) queue(conn quic.Connection, stream quic.Stream) {
	l.mu.Lock()
	lErr := l.lErr
	if l.lErr == nil {
		l.inbox = append(l.inbox, Conn{Conn: conn, Stream: stream})
		l.cond.Signal()
	}
	l.mu.Unlock()

	if lErr != nil {
		zerolog.Ctx(l.ctx).Info().Stringer("remote_addr", conn.RemoteAddr()).
			Stringer("local_addr", conn.LocalAddr()).Err(lErr).Msg("Listener was closed")
		conn.CloseWithError(0, "")
	}
}

func (l *Listener) Accept() (net.Conn, error) {
	l.mu.Lock()
	defer l.mu.Unlock()

	for len(l.inbox) == 0 {
		l.cond.Wait()

		if l.lErr != nil {
			return nil, l.lErr
		}

		if err := l.ctx.Err(); err != nil {
			return nil, err
		}
	}

	conn := l.inbox[0]
	l.inbox = l.inbox[1:]
	return conn, nil
}

func (l *Listener) Close() error {
	return l.closeWithErr(errClosed)
}

func (l *Listener) Addr() net.Addr {
	return l.listener.Addr()
}


================================================
FILE: pkg/quicproxy/proxyproto.go
================================================
package quicproxy

import (
	"context"
	"errors"
	"fmt"
	"math/rand"
	"net"
	"sync"

	proxyproto "github.com/pires/go-proxyproto"
	"github.com/rs/zerolog"
)

type ProxyProtoFrontend struct {
	ListenPort         int
	PortStart, PortEnd int
	PublicAddr         string

	mu    sync.RWMutex
	alloc map[int]func(net.Conn)
}

func (pf *ProxyProtoFrontend) ListenAndServe(ctx context.Context) error {
	var l net.ListenConfig
	lst, err := l.Listen(ctx, "tcp", fmt.Sprintf(":%d", pf.ListenPort))
	if err != nil {
		return err
	}

	go func() {
		<-ctx.Done()
		_ = lst.Close()
	}()

	proxyListener := &proxyproto.Listener{Listener: lst}

	for {
		conn, err := proxyListener.Accept()
		if err != nil {
			return err
		}

		l := zerolog.Ctx(ctx).With().Stringer("remote_addr", conn.RemoteAddr()).
			Stringer("local_addr", conn.LocalAddr()).Logger()

		if tcpaddr, ok := conn.LocalAddr().(*net.TCPAddr); ok {
			go func() {
				pf.mu.RLock()
				handler, ok := pf.alloc[tcpaddr.Port]
				if ok {
					l.Debug().Msg("New connection")
					// Call handler with the rlock held to make sure we're
					// always handling streams consistently. Handler will
					// quickly spawn a go routine and return.
					handler(conn)
				} else {
					l.Debug().Msg("No match")
				}
				pf.mu.RUnlock()

				// Close without holding the lock.
				if !ok {
					_ = conn.Close()
				}
			}()
		} else {
			l.Debug().Msg("Ignored non-tcp")
			_ = conn.Close()
		}
	}
}

func (pf *ProxyProtoFrontend) allocate(ctx context.Context, handler func(net.Conn)) (int, func(), error) {
	pf.mu.Lock()
	defer pf.mu.Unlock()

	// XXX naive; move to pre-shuffle.
	for i := 0; i < 100; i++ {
		port := pf.PortStart + rand.Int()%(pf.PortEnd-pf.PortStart)
		if _, ok := pf.alloc[port]; !ok {
			if pf.alloc == nil {
				pf.alloc = map[int]func(net.Conn){}
			}
			pf.alloc[port] = handler
			return port, func() {
				pf.mu.Lock()
				delete(pf.alloc, port)
				pf.mu.Unlock()
			}, nil
		}
	}

	return -1, nil, errors.New("failed to allocate port")
}

func (pf *ProxyProtoFrontend) Handle(ctx context.Context, handlers Handlers) error {
	port, cleanup, err := pf.allocate(ctx, func(conn net.Conn) {
		go handlers.HandleConn(conn)
	})

	if err != nil {
		return err
	}

	defer cleanup()

	alloc := Allocation{Endpoint: fmt.Sprintf("%s:%d", pf.PublicAddr, port)}

	if err := handlers.OnAllocation(alloc); err != nil {
		return err
	}

	<-ctx.Done()
	ctxErr := ctx.Err()

	if handlers.OnCleanup != nil {
		handlers.OnCleanup(alloc, ctxErr)
	}

	return ctxErr
}


================================================
FILE: pkg/quicproxy/rawproto.go
================================================
package quicproxy

import (
	"context"
	"fmt"
	"net"

	"github.com/rs/zerolog"
)

type RawFrontend struct {
	PublicAddr string
}

func (rf RawFrontend) ListenAndServe(ctx context.Context) error {
	return nil
}

func (rf RawFrontend) Handle(ctx context.Context, handlers Handlers) error {
	var d net.ListenConfig
	listener, err := d.Listen(ctx, "tcp", "0.0.0.0:0")
	if err != nil {
		return err
	}

	// If the context is canceled (e.g. the registration stream breaks), also
	// stop the listener.
	go func() {
		<-ctx.Done()
		_ = listener.Close()
	}()

	// If we leave the Serve handler for reasons other than the listener
	// closing, make sure it's closed.
	defer func() {
		_ = listener.Close()
	}()

	port := listener.Addr().(*net.TCPAddr).Port
	alloc := Allocation{Endpoint: fmt.Sprintf("%s:%d", rf.PublicAddr, port)}

	if err := handlers.OnAllocation(alloc); err != nil {
		return err
	}

	for {
		conn, err := listener.Accept()
		if err != nil {
			if handlers.OnCleanup != nil {
				handlers.OnCleanup(alloc, err)
			}
			return err
		}

		zerolog.Ctx(ctx).Debug().Stringer("remote_addr", conn.RemoteAddr()).
			Stringer("local_addr", conn.LocalAddr()).
			Str("allocation", alloc.Endpoint).Msg("New connection")

		go handlers.HandleConn(conn)
	}
}


================================================
FILE: pkg/quicproxy/serve.go
================================================
package quicproxy

import (
	"context"
	"errors"
	"net"
	"time"

	"github.com/quic-go/quic-go"
	"github.com/rs/zerolog"
	"inet.af/tcpproxy"
	"namespacelabs.dev/breakpoint/pkg/quicnet"
)

type Allocation struct {
	Endpoint string
}

type ProxyFrontend interface {
	ListenAndServe(context.Context) error
	Handle(context.Context, Handlers) error
}

type Handlers struct {
	OnAllocation func(Allocation) error
	OnCleanup    func(Allocation, error)
	HandleConn   func(net.Conn)
}

func ServeProxy(ctx context.Context, frontend ProxyFrontend, conn quic.Connection, callback func(Allocation) error) error {
	backend := tcpproxy.To("backend")
	backend.DialTimeout = 30 * time.Second
	backend.ProxyProtocolVersion = 1
	backend.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
		return quicnet.OpenStream(ctx, conn)
	}

	return frontend.Handle(ctx, Handlers{
		OnAllocation: func(alloc Allocation) error {
			zerolog.Ctx(ctx).Info().Str("allocation", alloc.Endpoint).Msg("New allocation")
			return callback(alloc)
		},
		OnCleanup: func(alloc Allocation, err error) {
			zerolog.Ctx(ctx).Info().Str("allocation", alloc.Endpoint).Err(cancelIsOK(err)).Msg("Released allocation")
		},
		HandleConn: backend.HandleConn,
	})
}

func cancelIsOK(err error) error {
	if errors.Is(err, context.Canceled) {
		return nil
	}

	return err
}


================================================
FILE: pkg/quicproxy/service.go
================================================
package quicproxy

import (
	"context"
	"crypto/tls"
	"errors"
	"time"

	"github.com/MicahParks/keyfunc"
	"github.com/quic-go/quic-go"
	"github.com/rs/zerolog"
	"golang.org/x/exp/slices"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/peer"
	"google.golang.org/grpc/status"
	apipb "namespacelabs.dev/breakpoint/api/public/v1"
	"namespacelabs.dev/breakpoint/pkg/githuboidc"
	"namespacelabs.dev/breakpoint/pkg/quicgrpc"
	"namespacelabs.dev/breakpoint/pkg/quicnet"
	"namespacelabs.dev/breakpoint/pkg/quicproxyclient"
	"namespacelabs.dev/breakpoint/pkg/tlscerts"
)

type Server struct {
	p        ProxyFrontend
	listener quic.Listener
	ghJWKS   *keyfunc.JWKS
}

type ServerOpts struct {
	ProxyFrontend    ProxyFrontend
	ListenAddr       string
	Subjects         tlscerts.Subjects
	EnableGitHubOIDC bool
}

func NewServer(ctx context.Context, opts ServerOpts) (*Server, error) {
	t := time.Now()
	public, private, err := tlscerts.GenerateECDSAPair(opts.Subjects, 365*24*time.Hour)
	if err != nil {
		return nil, err
	}
	zerolog.Ctx(ctx).Info().Dur("took", time.Since(t)).Msg("Generated new keys")

	srv := &Server{p: opts.ProxyFrontend}

	if opts.EnableGitHubOIDC {
		t = time.Now()
		jwks, err := githuboidc.ProvideVerifier(ctx)
		if err != nil {
			return nil, err
		}
		zerolog.Ctx(ctx).Info().Dur("took", time.Since(t)).Msg("Prepared GitHub JWKS")
		srv.ghJWKS = jwks
	}

	cert, err := tls.X509KeyPair(public, private)
	if err != nil {
		return nil, err
	}

	tlsconf := &tls.Config{
		Certificates: []tls.Certificate{cert},
		NextProtos:   []string{apipb.QuicProto},
	}

	listener, err := quic.ListenAddr(opts.ListenAddr, tlsconf, quicproxyclient.DefaultConfig)
	if err != nil {
		return nil, err
	}

	srv.listener = *listener
	return srv, nil
}

func (srv *Server) Close() error {
	return srv.listener.Close()
}

func (srv *Server) Serve(ctx context.Context) error {
	zerolog.Ctx(ctx).Info().Str("addr", srv.listener.Addr().String()).Msg("Listening")

	grpcServer := grpc.NewServer(grpc.Creds(quicgrpc.QuicCreds{NonQuicCreds: insecure.NewCredentials()}))
	apipb.RegisterProxyServiceServer(grpcServer, server{
		logger:   zerolog.Ctx(ctx).With().Logger(),
		frontend: srv.p,
		ghJWKS:   srv.ghJWKS,
	})
	return grpcServer.Serve(quicnet.NewListener(ctx, srv.listener))
}

type server struct {
	apipb.UnimplementedProxyServiceServer

	logger   zerolog.Logger
	frontend ProxyFrontend
	ghJWKS   *keyfunc.JWKS

	restrictToRepositories []string
	restrictToOwners       []string
}

func (srv server) Register(req *apipb.RegisterRequest, server apipb.ProxyService_RegisterServer) error {
	peer, _ := peer.FromContext(server.Context())
	quic, ok := peer.AuthInfo.(quicgrpc.QuicAuthInfo)
	if !ok {
		return errors.New("internal error, expected quic")
	}

	githubClaims, logger := validateGitHubOIDC(server.Context(), srv.logger, srv.ghJWKS)

	if len(srv.restrictToRepositories) > 0 {
		if githubClaims == nil || !slices.Contains(srv.restrictToRepositories, githubClaims.Repository) {
			return status.Errorf(codes.PermissionDenied, "repository %q not allowed", githubClaims.Repository)
		}
	}

	if len(srv.restrictToOwners) > 0 {
		if githubClaims == nil || !slices.Contains(srv.restrictToOwners, githubClaims.RepositoryOwner) {
			return status.Errorf(codes.PermissionDenied, "repository owner %q not allowed", githubClaims.RepositoryOwner)
		}
	}

	return ServeProxy(logger.WithContext(server.Context()), srv.frontend, quic.Conn, func(alloc Allocation) error {
		return server.Send(&apipb.RegisterResponse{Endpoint: alloc.Endpoint})
	})
}

func validateGitHubOIDC(ctx context.Context, logger zerolog.Logger, jwks *keyfunc.JWKS) (*githuboidc.Claims, zerolog.Logger) {
	if jwks != nil {
		md, _ := metadata.FromIncomingContext(ctx)
		if token, ok := md[apipb.GitHubOIDCTokenHeader]; ok && len(token) > 0 {
			claims, err := githuboidc.Validate(ctx, jwks, token[0])

			if err != nil {
				logger.Warn().Err(err).Msg("Failed to validate GitHub OIDC Token")
			} else if slices.Contains(claims.Audience, apipb.GitHubOIDCAudience) {
				logger.Warn().Str("expected", apipb.GitHubOIDCAudience).Strs("audience", claims.Audience).
					Msg("Failed to validate GitHub OIDC Token audience")
			} else {
				return claims, logger.With().Str("repository", claims.Repository).Logger()
			}
		}
	}

	return nil, logger
}


================================================
FILE: pkg/quicproxyclient/client.go
================================================
package quicproxyclient

import (
	"context"
	"crypto/tls"
	"errors"
	"net"
	"time"

	proxyproto "github.com/pires/go-proxyproto"
	"github.com/quic-go/quic-go"
	"github.com/rs/zerolog"
	"golang.org/x/sync/errgroup"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/metadata"
	v1 "namespacelabs.dev/breakpoint/api/public/v1"
	"namespacelabs.dev/breakpoint/pkg/bgrpc"
	"namespacelabs.dev/breakpoint/pkg/quicnet"
)

var DefaultConfig = &quic.Config{
	MaxIdleTimeout:  5 * time.Second,
	KeepAlivePeriod: 30 * time.Second,
}

type Handlers struct {
	OnAllocation func(string)
	Proxy        func(net.Conn) error
}

func Serve(ctx context.Context, endpoint string, md metadata.MD, handlers Handlers) error {
	tlsConf := &tls.Config{
		InsecureSkipVerify: true,
		NextProtos:         []string{v1.QuicProto},
	}

	zerolog.Ctx(ctx).Info().Str("endpoint", endpoint).Msg("Connecting")

	conn, err := quic.DialAddr(ctx, endpoint, tlsConf, DefaultConfig)
	if err != nil {
		return err
	}

	grpconn, err := bgrpc.DialContext(ctx, endpoint,
		grpc.WithBlock(),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
			return quicnet.OpenStream(ctx, conn)
		}),
	)
	if err != nil {
		return err
	}

	cli := v1.NewProxyServiceClient(grpconn)

	rsrv, err := cli.Register(metadata.NewOutgoingContext(ctx, md), &v1.RegisterRequest{})
	if err != nil {
		return err
	}

	eg, ctx := errgroup.WithContext(ctx)

	eg.Go(func() error {
		for {
			stream, err := conn.AcceptStream(ctx)
			if err != nil {
				if !errors.Is(err, context.Canceled) {
					zerolog.Ctx(ctx).Err(err).Msg("accept failed")
				}
				return err
			}

			pconn := proxyproto.NewConn(quicnet.Conn{Stream: stream, Conn: conn})

			zerolog.Ctx(ctx).Info().Stringer("remote_addr", pconn.RemoteAddr()).
				Stringer("local_addr", pconn.LocalAddr()).Msg("New remote connection")

			if err := handlers.Proxy(pconn); err != nil {
				zerolog.Ctx(ctx).Err(err).Msg("handle failed")
				return err
			}
		}
	})

	eg.Go(func() error {
		for {
			msg, err := rsrv.Recv()
			if err != nil {
				return err
			}

			handlers.OnAllocation(msg.Endpoint)
		}
	})

	return eg.Wait()
}


================================================
FILE: pkg/sshd/keepalive.go
================================================
package sshd

import (
	"context"
	"errors"
	"io"
	"time"

	"github.com/gliderlabs/ssh"
	"github.com/rs/zerolog"
)

func keepAlive(ctx context.Context, logger zerolog.Logger, session ssh.Session) {
	t := time.NewTicker(15 * time.Second)
	defer t.Stop()

	for {
		select {
		case <-t.C:
			t := time.Now()
			if _, err := session.SendRequest("keepalive@openssh.com", true, nil); err != nil {
				if !errors.Is(err, io.EOF) {
					logger.Err(err).Msg("Failed to send keepalive")
				} else {
					return
				}
			} else {
				logger.Debug().Dur("took", time.Since(t)).Msg("Got KeepAlive response")
			}

		case <-ctx.Done():
			return
		}
	}
}


================================================
FILE: pkg/sshd/pty_unix.go
================================================
//go:build !windows

package sshd

import (
	"fmt"
	"io"
	"os"
	"os/exec"
	"syscall"
	"unsafe"

	"github.com/creack/pty"
	"github.com/gliderlabs/ssh"
)

func handlePty(session io.ReadWriter, ptyReq ssh.Pty, winCh <-chan ssh.Window, cmd *exec.Cmd) error {
	cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
	ptyFile, err := pty.Start(cmd)
	if err != nil {
		return err
	}

	defer ptyFile.Close()

	go syncWinSize(ptyFile, winCh)
	go func() {
		_, _ = io.Copy(ptyFile, session) // stdin
	}()
	_, _ = io.Copy(session, ptyFile) // stdout

	return nil
}

func syncWinSize(ptyFile *os.File, winCh <-chan ssh.Window) {
	for win := range winCh {
		setWinsize(ptyFile, win.Width, win.Height)
	}
}

func setWinsize(f *os.File, w, h int) {
	syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ),
		uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0})))
}


================================================
FILE: pkg/sshd/pty_windows.go
================================================
//go:build windows

package sshd

import (
	"errors"
	"io"
	"os/exec"

	"github.com/gliderlabs/ssh"
)

func handlePty(session io.ReadWriter, ptyReq ssh.Pty, winCh <-chan ssh.Window, cmd *exec.Cmd) error {
	return errors.New("pty not supported in windows")
}


================================================
FILE: pkg/sshd/sftp.go
================================================
package sshd

import (
	"io"

	"github.com/gliderlabs/ssh"
	"github.com/pkg/sftp"
	"github.com/rs/zerolog"
)

func makeSftpHandler(logger zerolog.Logger) ssh.SubsystemHandler {
	return func(sess ssh.Session) {
		server, err := sftp.NewServer(sess, sftp.WithDebug(io.Discard))
		if err != nil {
			logger.Err(err).Msg("sftp: failed to init server")
			return
		}

		defer server.Close()

		if err := server.Serve(); err != nil && err != io.EOF {
			logger.Err(err).Msg("sftp: session done with error")
		} else {
			logger.Info().Msg("sftp: session done")
		}
	}
}


================================================
FILE: pkg/sshd/sshd.go
================================================
package sshd

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"fmt"
	"io"
	"net"
	"os/exec"
	"runtime"
	"time"

	"github.com/gliderlabs/ssh"
	"github.com/rs/zerolog"
	"go.uber.org/atomic"
	gossh "golang.org/x/crypto/ssh"
	"golang.org/x/exp/maps"
	"golang.org/x/exp/slices"
)

type SSHServerOpts struct {
	AllowedUsers   []string
	AuthorizedKeys map[string]string // Key to owner
	Env            []string
	Shell          []string
	Dir            string

	InteractiveMOTD func(io.Writer)
}

type sshKey struct {
	Key   ssh.PublicKey
	Owner string
}

type SSHServer struct {
	Server         *ssh.Server
	NumConnections func() uint32
}

func MakeServer(ctx context.Context, opts SSHServerOpts) (*SSHServer, error) {
	var authorizedKeys []sshKey
	for key, owner := range opts.AuthorizedKeys {
		key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
		if err != nil {
			return nil, err
		}
		authorizedKeys = append(authorizedKeys, sshKey{key, owner})
	}

	l := zerolog.Ctx(ctx).With().Str("service", "sshd").Logger()

	connCount := atomic.NewUint32(0)

	srv := &ssh.Server{
		Handler: func(session ssh.Session) {
			key, _ := lookupKey(authorizedKeys, session.PublicKey())
			sessionLog := l.With().Stringer("remote_addr", session.RemoteAddr()).Str("owner", key.Owner).Logger()

			sessionLog.Info().Str("user", session.User()).Msg("incoming ssh session")

			args := opts.Shell[1:]
			if session.RawCommand() != "" {
				if runtime.GOOS == "windows" {
					args = []string{"/C", session.RawCommand()}
				} else {
					args = []string{"-c", session.RawCommand()}
				}
			}

			cmd := exec.Command(opts.Shell[0], args...)
			cmd.Env = slices.Clone(opts.Env)
			cmd.Dir = opts.Dir

			if ssh.AgentRequested(session) {
				l, err := ssh.NewAgentListener()
				if err != nil {
					fmt.Fprintf(session, "Failed to forward agent.\n")
				} else {
					defer l.Close()
					go ssh.ForwardAgentConnections(l, session)
					cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", "SSH_AUTH_SOCK", l.Addr().String()))
				}
			}

			ptyReq, winCh, isPty := session.Pty()

			sessionLog.Info().Bool("ssh_agent", ssh.AgentRequested(session)).Bool("pty", isPty).Msg("ssh session")

			ctx, cancel := context.WithCancel(session.Context())
			defer cancel()

			// Make sure that the connection with the client is kept alive.
			go keepAlive(ctx, sessionLog, session)

			if isPty {
				// Print MOTD only if no command was provided
				if opts.InteractiveMOTD != nil && session.RawCommand() == "" {
					opts.InteractiveMOTD(session)
				}

				if err := handlePty(session, ptyReq, winCh, cmd); err != nil {
					sessionLog.Err(err).Msg("pty start failed")
					session.Exit(1)
					return
				}
			} else {
				cmd.Stdout = session
				cmd.Stderr = session
				if err := cmd.Start(); err != nil {
					sessionLog.Err(err).Msg("start failed")
					session.Exit(1)
					return
				}
			}

			// XXX pass exit code to caller?
			err := cmd.Wait()
			sessionLog.Info().Err(err).Msg("ssh session end")
		},

		SessionRequestCallback: func(sess ssh.Session, requestType string) bool {
			return len(opts.AllowedUsers) == 0 || slices.Contains(opts.AllowedUsers, sess.User())
		},

		PublicKeyHandler: func(ctx ssh.Context, key ssh.PublicKey) bool {
			_, allowed := lookupKey(authorizedKeys, key)
			return allowed
		},

		LocalPortForwardingCallback: func(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {
			sessionLog := l.With().Stringer("remote_addr", ctx.RemoteAddr()).Logger()
			sessionLog.Info().Str("dst", fmt.Sprintf("%s:%d", destinationHost, destinationPort)).Msg("Port forward request")
			return true
		},

		SubsystemHandlers: map[string]ssh.SubsystemHandler{
			"sftp": makeSftpHandler(l),
		},

		ConnCallback: func(ctx ssh.Context, conn net.Conn) net.Conn {
			connCount.Inc()
			go func() {
				<-ctx.Done()
				connCount.Dec()
			}()

			return conn
		},
	}

	srv.ChannelHandlers = maps.Clone(ssh.DefaultChannelHandlers)
	srv.ChannelHandlers["direct-tcpip"] = ssh.DirectTCPIPHandler

	t := time.Now()
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return nil, err
	}

	signer, err := gossh.NewSignerFromKey(key)
	if err != nil {
		return nil, err
	}

	srv.HostSigners = append(srv.HostSigners, signer)

	zerolog.Ctx(ctx).Info().Str("host_key_fingerprint", gossh.FingerprintSHA256(signer.PublicKey())).Dur("took", time.Since(t)).Msg("Generated ssh host key")

	return &SSHServer{
		Server:         srv,
		NumConnections: connCount.Load,
	}, nil
}

func lookupKey(allowed []sshKey, key ssh.PublicKey) (sshKey, bool) {
	for _, allowed := range allowed {
		if ssh.KeysEqual(key, allowed.Key) {
			return allowed, true
		}
	}
	return sshKey{}, false
}


================================================
FILE: pkg/tlscerts/tlscerts.go
================================================
package tlscerts

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/x509"
	"encoding/pem"
	"math/big"
	"net"
	"time"
)

type Subjects struct {
	DNSNames    []string
	IPAddresses []net.IP
}

func GenerateECDSAPair(subjects Subjects, duration time.Duration) ([]byte, []byte, error) {
	serial, err := newSerialNumber()
	if err != nil {
		return nil, nil, err
	}

	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return nil, nil, err
	}

	privDer, err := x509.MarshalPKCS8PrivateKey(priv)
	if err != nil {
		return nil, nil, err
	}

	privPem := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privDer})

	template := &x509.Certificate{
		SerialNumber: serial,
		NotAfter:     time.Now().Add(duration),
		DNSNames:     subjects.DNSNames,
		IPAddresses:  subjects.IPAddresses,
	}

	certDer, err := x509.CreateCertificate(rand.Reader, template, template, priv.Public(), priv)
	if err != nil {
		return nil, nil, err
	}

	certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDer})
	if err != nil {
		return nil, nil, err
	}

	return certPem, privPem, nil
}

func newSerialNumber() (*big.Int, error) {
	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
	return rand.Int(rand.Reader, serialNumberLimit)
}


================================================
FILE: pkg/waiter/output.go
================================================
package waiter

import (
	"fmt"
	"io"
	"net"
	"strings"
	"time"

	"github.com/dustin/go-humanize"
	"github.com/muesli/reflow/wordwrap"
)

func PrintConnectionInfo(endpoint string, deadline time.Time, output io.Writer) {
	host, port, _ := net.SplitHostPort(endpoint)

	if host == "" && port == "" {
		return
	}

	ww := wordwrap.NewWriter(80)
	fmt.Fprintf(ww, "Breakpoint! Running until %v (%v).", deadline.Format(Stamp), humanize.Time(deadline))
	_ = ww.Close()

	lines := strings.Split(ww.String(), "\n")

	longestLine := 0
	for _, l := range lines {
		if len(l) > longestLine {
			longestLine = len(l)
		}
	}

	longline := nchars('─', longestLine)
	spaces := nchars(' ', longestLine)
	fmt.Fprintln(output)
	fmt.Fprintf(output, "┌─%s─┐\n", longline)
	for _, l := range lines {
		fmt.Fprintf(output, "│ %s%s │\n", l, spaces[len(l):])
	}
	fmt.Fprintf(output, "└─%s─┘\n", longline)
	fmt.Fprintln(output)

	fmt.Fprintf(output, "Connect with:\n\n")
	fmt.Fprintf(output, "ssh -p %s runner@%s\n", port, host)
}


================================================
FILE: pkg/waiter/slackbot.go
================================================
package waiter

import (
	"context"
	"fmt"
	"net"
	"os"
	"time"

	"github.com/dustin/go-humanize"
	"github.com/google/go-github/v52/github"
	"github.com/rs/zerolog"
	"github.com/slack-go/slack"
	v1 "namespacelabs.dev/breakpoint/api/private/v1"
	"namespacelabs.dev/breakpoint/pkg/jsonfile"
)

type botInstance struct {
	client      *slack.Client
	m           *Manager
	githubProps renderGitHubProps

	channelID string
	ts        string
}

func startBot(ctx context.Context, m *Manager, conf v1.SlackBot) *botInstance {
	bot := &botInstance{
		client:      slack.New(os.ExpandEnv(conf.Token)),
		m:           m,
		githubProps: prepareGitHubProps(ctx),
	}

	chid, ts, err := bot.client.PostMessageContext(ctx, os.ExpandEnv(conf.Channel), bot.makeBlocks(false))
	if err != nil {
		zerolog.Ctx(ctx).Err(err).Msg("SlackBot failed")
		return nil
	}

	bot.channelID = chid
	bot.ts = ts

	go bot.loop(ctx)

	return bot
}

func (b *botInstance) Close() error {
	ctx, done := context.WithTimeout(context.Background(), 5*time.Second)
	defer done()

	return b.sendUpdate(ctx, true)
}

func (b *botInstance) makeBlocks(leaving bool) slack.MsgOption {
	if leaving {
		return slack.MsgOptionBlocks(renderGitHubMessage(b.githubProps, "", time.Time{})...)
	}

	return slack.MsgOptionBlocks(renderGitHubMessage(b.githubProps, b.m.Endpoint(), b.m.Expiration())...)
}

func (b *botInstance) sendUpdate(ctx context.Context, leaving bool) error {
	_, _, _, err := b.client.UpdateMessageContext(ctx, b.channelID, b.ts, b.makeBlocks(leaving))
	return err
}

func (b *botInstance) loop(ctx context.Context) error {
	t := time.NewTicker(30 * time.Second)
	defer t.Stop()

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()

		case <-t.C:
			if err := b.sendUpdate(ctx, false); err != nil {
				return err
			}
		}
	}
}

type renderGitHubProps struct {
	Repository string
	RefName    string
	Workflow   string
	RunID      string
	RunNumber  string
	Actor      string
	PushEvent  *github.PushEvent // Only set on push events.
}

func prepareGitHubProps(ctx context.Context) renderGitHubProps {
	props := renderGitHubProps{
		Repository: os.Getenv("GITHUB_REPOSITORY"),
		RefName:    os.Getenv("GITHUB_REF_NAME"),
		Workflow:   os.Getenv("GITHUB_WORKFLOW"),
		RunID:      os.Getenv("GITHUB_RUN_ID"),
		RunNumber:  os.Getenv("GITHUB_RUN_NUMBER"),
		Actor:      os.Getenv("GITHUB_ACTOR"),
	}

	if eventFile := os.Getenv("GITHUB_EVENT_PAH"); os.Getenv("GITHUB_EVENT_NAME") == "push" && eventFile != "" {
		var pushEvent github.PushEvent
		if err := jsonfile.Load(eventFile, &pushEvent); err != nil {
			zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to load event file")
		} else {
			props.PushEvent = &pushEvent
		}
	}

	return props
}

func renderGitHubMessage(props renderGitHubProps, endpoint string, exp time.Time) []slack.Block {
	blocks := []slack.Block{
		slack.NewHeaderBlock(slack.NewTextBlockObject(slack.PlainTextType, "Workflow failed", false, false)),
		slack.NewSectionBlock(slack.NewTextBlockObject(
			slack.MarkdownType,
			fmt.Sprintf("*Repository:* <https://github.com/%s/tree/%s|github.com/%s> (%s)", props.Repository, props.RefName, props.Repository, props.RefName),
			false, false,
		), nil, nil),
		slack.NewSectionBlock(slack.NewTextBlockObject(
			slack.MarkdownType,
			fmt.Sprintf("*Workflow:* %s (<https://github.com/%s/actions/runs/%s|Run #%s>)", props.Workflow, props.Repository, props.RunID, props.RunNumber),
			false, false,
		), nil, nil),
	}

	if props.PushEvent != nil && props.PushEvent.HeadCommit != nil && props.PushEvent.HeadCommit.Message != nil {
		blocks = append(blocks,
			slack.NewSectionBlock(slack.NewTextBlockObject(
				slack.MarkdownType,
				fmt.Sprintf("*<%s|Commit>:* %s`", maybeCommitURL(props.Repository, *props.PushEvent), *props.PushEvent.HeadCommit.Message),
				false, false,
			), nil, nil))
	}

	if endpoint != "" && !exp.IsZero() {
		host, port, _ := net.SplitHostPort(endpoint)

		blocks = append(blocks,
			slack.NewSectionBlock(slack.NewTextBlockObject(
				slack.MarkdownType,
				fmt.Sprintf("*SSH:* `ssh -p %s runner@%s`", port, host),
				false, false,
			), nil, nil),
			slack.NewSectionBlock(slack.NewTextBlockObject(
				slack.MarkdownType,
				fmt.Sprintf("*Expires:* %s (%s)", humanize.Time(exp), exp.Format(Stamp)),
				false, false,
			), nil, nil),
		)
	}

	blocks = append(blocks, slack.NewContextBlock("",
		slack.NewTextBlockObject(slack.PlainTextType, fmt.Sprintf("Actor: %s", props.Actor), false, false)))

	return blocks
}

func maybeCommitURL(repo string, event github.PushEvent) string {
	if event.HeadCommit == nil || event.HeadCommit.URL == nil {
		if event.Repo == nil {
			return "https://github.com/" + repo
		}

		return *event.Repo.URL
	}

	return *event.HeadCommit.URL
}


================================================
FILE: pkg/waiter/template.go
================================================
package waiter

import (
	"os"
)

func execTemplate(value any, mapping func(string) string) any {
	if value == nil {
		return nil
	}

	switch x := value.(type) {
	case map[string]any:
		return execMapTemplate(x, mapping)

	case string:
		return os.Expand(x, mapping)

	case []any:
		var res []any
		for _, y := range x {
			res = append(res, execTemplate(y, mapping))
		}
		return res

	default:
	}

	return value
}

func execMapTemplate(input map[string]any, mapping func(string) string) map[string]any {
	if input == nil {
		return nil
	}

	out := map[string]any{}
	for key, value := range input {
		out[key] = execTemplate(value, mapping)
	}

	return out
}


================================================
FILE: pkg/waiter/template_test.go
================================================
package waiter

import (
	"encoding/json"
	"testing"

	"github.com/google/go-cmp/cmp"
	v1 "namespacelabs.dev/breakpoint/api/private/v1"
)

func TestExecTemplate(t *testing.T) {
	var webhook v1.Webhook

	if err := json.Unmarshal([]byte(`{
	"url": "foobar",
	"payload": {
        "blocks": [
          {
            "type": "header",
            "text": {
              "type": "plain_text",
              "text": "Workflow failed",
              "emoji": true
            }
          },
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "*Repository:* <https://${GITHUB_REPOSITORY}/tree/${GITHUB_REF_NAME}|${GITHUB_REPOSITORY}> (${GITHUB_REF_NAME})"
            }
          }
		]
	}
}`), &webhook); err != nil {
		t.Fatal(err)
	}

	got := execTemplate(webhook.Payload, func(str string) string {
		switch str {
		case "GITHUB_REPOSITORY":
			return "arepo"

		case "GITHUB_REF_NAME":
			return "main"
		}

		return ""
	})

	if d := cmp.Diff(map[string]any{
		"blocks": []any{
			map[string]any{
				"text": map[string]any{
					"emoji": bool(true),
					"text":  string("Workflow failed"),
					"type":  string("plain_text"),
				},
				"type": string("header"),
			},
			map[string]any{
				"text": map[string]any{
					"text": string("*Repository:* <https://arepo/tree/main|arepo> (main)"),
					"type": string("mrkdwn"),
				},
				"type": string("section"),
			},
		},
	}, got); d != "" {
		t.Errorf("mismatch (-want +got):\n%s", d)
	}

}


================================================
FILE: pkg/waiter/waiter.go
================================================
package waiter

import (
	"context"
	"io"
	"math"
	"net"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/dustin/go-humanize"
	"github.com/rs/zerolog"
	v1 "namespacelabs.dev/breakpoint/api/private/v1"
	"namespacelabs.dev/breakpoint/pkg/webhook"
)

const (
	logTickInterval = 1 * time.Minute

	Stamp = time.Stamp + " MST"
)

type ManagerOpts struct {
	InitialDur time.Duration

	Webhooks  []v1.Webhook
	SlackBots []v1.SlackBot
}

type ManagerStatus struct {
	Endpoint       string    `json:"endpoint"`
	Expiration     time.Time `json:"expiration"`
	NumConnections uint32    `json:"num_connections"`
}

type Manager struct {
	ctx    context.Context
	logger zerolog.Logger

	opts ManagerOpts

	mu                      sync.Mutex
	updated                 chan struct{}
	expiration              time.Time
	endpoint                string
	resources               []io.Closer
	connectionCountCallback func() uint32
}

func NewManager(ctx context.Context, opts ManagerOpts) (*Manager, context.Context) {
	ctx, cancel := context.WithCancel(ctx)
	l := zerolog.Ctx(ctx).With().Logger()
	m := &Manager{
		ctx:        ctx,
		logger:     l,
		opts:       opts,
		updated:    make(chan struct{}, 1),
		expiration: time.Now().Add(opts.InitialDur),
	}

	go func() {
		defer cancel()
		m.loop(ctx)

		m.mu.Lock()
		resources := m.resources
		m.resources = nil
		m.mu.Unlock()

		// Resources should clean up quickly as they hold up the cancelation of the context.
		// We're guaranteed to wait for these because the incoming `ctx` is never cancelled.
		for _, closer := range resources {
			if err := closer.Close(); err != nil {
				l.Err(err).Msg("Failed while cleaning up resource")
			}
		}
	}()

	return m, ctx
}

func (m *Manager) Wait() error {
	<-m.ctx.Done()
	return m.ctx.Err()
}

func (m *Manager) loop(ctx context.Context) {
	exitTimer := time.NewTicker(time.Until(m.expiration))
	defer exitTimer.Stop()

	logTicker := time.NewTicker(logTick())
	defer logTicker.Stop()

	for {
		select {
		case _, ok := <-m.updated:
			if !ok {
				return
			}

			m.mu.Lock()
			newExp := m.expiration
			m.mu.Unlock()

			exitTimer.Reset(time.Until(newExp))
			m.announce()

		case <-exitTimer.C:
			// Timer has expired, terminate the program
			m.logger.Info().Msg("Breakpoint expired")
			return

		case <-logTicker.C:
			m.announce()

		case <-ctx.Done():
			return
		}
	}
}

func logTick() time.Duration {
	// If running in CI, announce on a regular basis.
	if os.Getenv("CI") != "" {
		return logTickInterval
	}

	return math.MaxInt64
}

func (m *Manager) ExtendWait(dur time.Duration) time.Time {
	m.mu.Lock()
	defer m.mu.Unlock()

	m.expiration = m.expiration.Add(dur)

	m.updated <- struct{}{}

	m.logger.Info().
		Dur("dur", dur).
		Time("expiration", m.expiration).
		Msg("Extend wait")
	return m.expiration
}

func (m *Manager) StopWait() {
	m.logger.Info().Msg("Resume requested")
	close(m.updated)
}

func (m *Manager) Expiration() time.Time {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.expiration
}

func (m *Manager) Endpoint() string {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.endpoint
}

func (m *Manager) Status() ManagerStatus {
	m.mu.Lock()
	defer m.mu.Unlock()
	return ManagerStatus{
		Endpoint:       m.endpoint,
		Expiration:     m.expiration,
		NumConnections: m.connectionCountCallback(),
	}
}

func (m *Manager) SetEndpoint(addr string) {
	m.mu.Lock()
	m.endpoint = addr
	m.mu.Unlock()

	var resources []io.Closer
	for _, bot := range m.opts.SlackBots {
		if bot := startBot(m.ctx, m, bot); bot != nil {
			resources = append(resources, bot)
		}
	}

	m.mu.Lock()
	m.resources = resources
	m.mu.Unlock()

	m.updated <- struct{}{}

	expandf := expand(addr, m.Expiration())

	for _, wh := range m.opts.Webhooks {
		ctx, done := context.WithTimeout(m.ctx, 30*time.Second)
		defer done()

		payload := execTemplate(wh.Payload, expandf)

		t := time.Now()
		if err := webhook.Notify(ctx, os.Expand(wh.URL, expandf), payload); err != nil {
			m.logger.Err(err).Msg("Failed to notify Webhook")
		} else {
			m.logger.Info().Dur("took", time.Since(t)).Str("url", wh.URL).Msg("Notified webhook")
		}
	}
}

func (m *Manager) SetConnectionCountCallback(callback func() uint32) {
	m.mu.Lock()
	m.connectionCountCallback = callback
	m.mu.Unlock()
}

func expand(addr string, exp time.Time) func(key string) string {
	host, port, _ := net.SplitHostPort(addr)

	return func(key string) string {
		switch key {
		case "BREAKPOINT_ENDPOINT":
			return addr

		case "BREAKPOINT_HOST":
			return host

		case "BREAKPOINT_PORT":
			return port

		case "BREAKPOINT_TIME_LEFT":
			return strings.TrimSpace(humanize.RelTime(exp, time.Now(), "", ""))

		case "BREAKPOINT_EXPIRATION":
			return exp.Format(Stamp)
		}

		return os.Getenv(key)
	}
}

func (m *Manager) announce() {
	status := m.Status()
	PrintConnectionInfo(status.Endpoint, status.Expiration, os.Stderr)
}

func nchars(ch rune, n int) string {
	str := make([]rune, n)
	for k := 0; k < n; k++ {
		str[k] = ch
	}
	return string(str)
}


================================================
FILE: pkg/webhook/notifier.go
================================================
package webhook

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"

	"namespacelabs.dev/breakpoint/pkg/httperrors"
)

const (
	userAgent = "Breakpoint/1.0"
)

func Notify(ctx context.Context, endpoint string, payload any) error {
	body, err := json.Marshal(payload)
	if err != nil {
		return err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(body))
	if err != nil {
		return err
	}

	req.Header.Set("User-Agent", userAgent)
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}

	defer resp.Body.Close()

	return httperrors.MaybeError(resp)
}
Download .txt
gitextract_v5sic7m5/

├── .dockerignore
├── .github/
│   ├── slack-notification.json
│   └── workflows/
│       ├── build.yml
│       ├── checks.yml
│       ├── goreleaser.yml
│       └── release.yml
├── .gitignore
├── .goreleaser.yaml
├── Dockerfile
├── LICENSE
├── README.md
├── api/
│   ├── private/
│   │   └── v1/
│   │       ├── configtype.go
│   │       ├── service.pb.go
│   │       ├── service.proto
│   │       └── service_grpc.pb.go
│   └── public/
│       └── v1/
│           ├── service.pb.go
│           ├── service.proto
│           ├── service_grpc.pb.go
│           └── types.go
├── buf.gen.yaml
├── cmd/
│   ├── breakpoint/
│   │   ├── attach.go
│   │   ├── extend.go
│   │   ├── hold.go
│   │   ├── main.go
│   │   ├── resume.go
│   │   ├── start.go
│   │   ├── status.go
│   │   └── wait.go
│   └── rendezvous/
│       └── main.go
├── docs/
│   ├── CONTRIBUTING.md
│   └── server-setup.md
├── examples/
│   └── wait.withslack.json
├── flake.nix
├── fly.toml
├── go.mod
├── go.sum
└── pkg/
    ├── README.md
    ├── bcontrol/
    │   └── client.go
    ├── bgrpc/
    │   └── bgrpc.go
    ├── blog/
    │   └── blog.go
    ├── config/
    │   └── config.go
    ├── execbackground/
    │   ├── bg_unix.go
    │   └── bg_windows.go
    ├── github/
    │   └── sshkeys.go
    ├── githuboidc/
    │   ├── claims.go
    │   ├── gh.go
    │   └── verifier.go
    ├── httperrors/
    │   └── httperrors.go
    ├── internalserver/
    │   └── internalserver.go
    ├── jsonfile/
    │   └── load.go
    ├── passthrough/
    │   └── listener.go
    ├── quicgrpc/
    │   └── grpccreds.go
    ├── quicnet/
    │   ├── conn.go
    │   └── listener.go
    ├── quicproxy/
    │   ├── proxyproto.go
    │   ├── rawproto.go
    │   ├── serve.go
    │   └── service.go
    ├── quicproxyclient/
    │   └── client.go
    ├── sshd/
    │   ├── keepalive.go
    │   ├── pty_unix.go
    │   ├── pty_windows.go
    │   ├── sftp.go
    │   └── sshd.go
    ├── tlscerts/
    │   └── tlscerts.go
    ├── waiter/
    │   ├── output.go
    │   ├── slackbot.go
    │   ├── template.go
    │   ├── template_test.go
    │   └── waiter.go
    └── webhook/
        └── notifier.go
Download .txt
SYMBOL INDEX (253 symbols across 49 files)

FILE: api/private/v1/configtype.go
  type WaitConfig (line 3) | type WaitConfig struct
  type Webhook (line 15) | type Webhook struct
  type SlackBot (line 20) | type SlackBot struct

FILE: api/private/v1/service.pb.go
  constant _ (line 21) | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
  constant _ (line 23) | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
  type ExtendRequest (line 26) | type ExtendRequest struct
    method Reset (line 34) | func (x *ExtendRequest) Reset() {
    method String (line 43) | func (x *ExtendRequest) String() string {
    method ProtoMessage (line 47) | func (*ExtendRequest) ProtoMessage() {}
    method ProtoReflect (line 49) | func (x *ExtendRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 62) | func (*ExtendRequest) Descriptor() ([]byte, []int) {
    method GetWaitFor (line 66) | func (x *ExtendRequest) GetWaitFor() *durationpb.Duration {
  type ExtendResponse (line 73) | type ExtendResponse struct
    method Reset (line 81) | func (x *ExtendResponse) Reset() {
    method String (line 90) | func (x *ExtendResponse) String() string {
    method ProtoMessage (line 94) | func (*ExtendResponse) ProtoMessage() {}
    method ProtoReflect (line 96) | func (x *ExtendResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 109) | func (*ExtendResponse) Descriptor() ([]byte, []int) {
    method GetExpiration (line 113) | func (x *ExtendResponse) GetExpiration() *timestamppb.Timestamp {
  type StatusResponse (line 120) | type StatusResponse struct
    method Reset (line 130) | func (x *StatusResponse) Reset() {
    method String (line 139) | func (x *StatusResponse) String() string {
    method ProtoMessage (line 143) | func (*StatusResponse) ProtoMessage() {}
    method ProtoReflect (line 145) | func (x *StatusResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 158) | func (*StatusResponse) Descriptor() ([]byte, []int) {
    method GetExpiration (line 162) | func (x *StatusResponse) GetExpiration() *timestamppb.Timestamp {
    method GetEndpoint (line 169) | func (x *StatusResponse) GetEndpoint() string {
    method GetNumConnections (line 176) | func (x *StatusResponse) GetNumConnections() uint32 {
  function file_api_private_v1_service_proto_rawDescGZIP (line 242) | func file_api_private_v1_service_proto_rawDescGZIP() []byte {
  function init (line 275) | func init() { file_api_private_v1_service_proto_init() }
  function file_api_private_v1_service_proto_init (line 276) | func file_api_private_v1_service_proto_init() {

FILE: api/private/v1/service_grpc.pb.go
  constant _ (line 20) | _ = grpc.SupportPackageIsVersion7
  constant ControlService_Resume_FullMethodName (line 23) | ControlService_Resume_FullMethodName = "/namespacelabs.breakpoint.privat...
  constant ControlService_Extend_FullMethodName (line 24) | ControlService_Extend_FullMethodName = "/namespacelabs.breakpoint.privat...
  constant ControlService_Status_FullMethodName (line 25) | ControlService_Status_FullMethodName = "/namespacelabs.breakpoint.privat...
  type ControlServiceClient (line 31) | type ControlServiceClient interface
  type controlServiceClient (line 37) | type controlServiceClient struct
    method Resume (line 45) | func (c *controlServiceClient) Resume(ctx context.Context, in *emptypb...
    method Extend (line 54) | func (c *controlServiceClient) Extend(ctx context.Context, in *ExtendR...
    method Status (line 63) | func (c *controlServiceClient) Status(ctx context.Context, in *emptypb...
  function NewControlServiceClient (line 41) | func NewControlServiceClient(cc grpc.ClientConnInterface) ControlService...
  type ControlServiceServer (line 75) | type ControlServiceServer interface
  type UnimplementedControlServiceServer (line 83) | type UnimplementedControlServiceServer struct
    method Resume (line 86) | func (UnimplementedControlServiceServer) Resume(context.Context, *empt...
    method Extend (line 89) | func (UnimplementedControlServiceServer) Extend(context.Context, *Exte...
    method Status (line 92) | func (UnimplementedControlServiceServer) Status(context.Context, *empt...
    method mustEmbedUnimplementedControlServiceServer (line 95) | func (UnimplementedControlServiceServer) mustEmbedUnimplementedControl...
  type UnsafeControlServiceServer (line 100) | type UnsafeControlServiceServer interface
  function RegisterControlServiceServer (line 104) | func RegisterControlServiceServer(s grpc.ServiceRegistrar, srv ControlSe...
  function _ControlService_Resume_Handler (line 108) | func _ControlService_Resume_Handler(srv interface{}, ctx context.Context...
  function _ControlService_Extend_Handler (line 126) | func _ControlService_Extend_Handler(srv interface{}, ctx context.Context...
  function _ControlService_Status_Handler (line 144) | func _ControlService_Status_Handler(srv interface{}, ctx context.Context...

FILE: api/public/v1/service.pb.go
  constant _ (line 18) | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
  constant _ (line 20) | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
  type RegisterRequest (line 23) | type RegisterRequest struct
    method Reset (line 29) | func (x *RegisterRequest) Reset() {
    method String (line 38) | func (x *RegisterRequest) String() string {
    method ProtoMessage (line 42) | func (*RegisterRequest) ProtoMessage() {}
    method ProtoReflect (line 44) | func (x *RegisterRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 57) | func (*RegisterRequest) Descriptor() ([]byte, []int) {
  type RegisterResponse (line 61) | type RegisterResponse struct
    method Reset (line 69) | func (x *RegisterResponse) Reset() {
    method String (line 78) | func (x *RegisterResponse) String() string {
    method ProtoMessage (line 82) | func (*RegisterResponse) ProtoMessage() {}
    method ProtoReflect (line 84) | func (x *RegisterResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 97) | func (*RegisterResponse) Descriptor() ([]byte, []int) {
    method GetEndpoint (line 101) | func (x *RegisterResponse) GetEndpoint() string {
  function file_api_public_v1_service_proto_rawDescGZIP (line 137) | func file_api_public_v1_service_proto_rawDescGZIP() []byte {
  function init (line 159) | func init() { file_api_public_v1_service_proto_init() }
  function file_api_public_v1_service_proto_init (line 160) | func file_api_public_v1_service_proto_init() {

FILE: api/public/v1/service_grpc.pb.go
  constant _ (line 19) | _ = grpc.SupportPackageIsVersion7
  constant ProxyService_Register_FullMethodName (line 22) | ProxyService_Register_FullMethodName = "/namespacelabs.breakpoint.ProxyS...
  type ProxyServiceClient (line 28) | type ProxyServiceClient interface
  type proxyServiceClient (line 33) | type proxyServiceClient struct
    method Register (line 41) | func (c *proxyServiceClient) Register(ctx context.Context, in *Registe...
  function NewProxyServiceClient (line 37) | func NewProxyServiceClient(cc grpc.ClientConnInterface) ProxyServiceClie...
  type ProxyService_RegisterClient (line 56) | type ProxyService_RegisterClient interface
  type proxyServiceRegisterClient (line 61) | type proxyServiceRegisterClient struct
    method Recv (line 65) | func (x *proxyServiceRegisterClient) Recv() (*RegisterResponse, error) {
  type ProxyServiceServer (line 76) | type ProxyServiceServer interface
  type UnimplementedProxyServiceServer (line 83) | type UnimplementedProxyServiceServer struct
    method Register (line 86) | func (UnimplementedProxyServiceServer) Register(*RegisterRequest, Prox...
    method mustEmbedUnimplementedProxyServiceServer (line 89) | func (UnimplementedProxyServiceServer) mustEmbedUnimplementedProxyServ...
  type UnsafeProxyServiceServer (line 94) | type UnsafeProxyServiceServer interface
  function RegisterProxyServiceServer (line 98) | func RegisterProxyServiceServer(s grpc.ServiceRegistrar, srv ProxyServic...
  function _ProxyService_Register_Handler (line 102) | func _ProxyService_Register_Handler(srv interface{}, stream grpc.ServerS...
  type ProxyService_RegisterServer (line 110) | type ProxyService_RegisterServer interface
  type proxyServiceRegisterServer (line 115) | type proxyServiceRegisterServer struct
    method Send (line 119) | func (x *proxyServiceRegisterServer) Send(m *RegisterResponse) error {

FILE: api/public/v1/types.go
  constant QuicProto (line 4) | QuicProto = "breakpoint-grpc"
  constant GitHubOIDCTokenHeader (line 6) | GitHubOIDCTokenHeader = "x-breakpoint-github-oidc-token"
  constant GitHubOIDCAudience (line 8) | GitHubOIDCAudience = "namespacelabs.dev/breakpoint"

FILE: cmd/breakpoint/attach.go
  function newAttachCmd (line 13) | func newAttachCmd() *cobra.Command {
  function init (line 45) | func init() {

FILE: cmd/breakpoint/extend.go
  function newExtendCmd (line 16) | func newExtendCmd() *cobra.Command {
  function init (line 61) | func init() {

FILE: cmd/breakpoint/hold.go
  function init (line 18) | func init() {
  constant extendBy (line 23) | extendBy = 30 * time.Second
  function newHoldCmd (line 26) | func newHoldCmd() *cobra.Command {
  function holdForDuration (line 69) | func holdForDuration(ctx context.Context, duration time.Duration) error {
  function holdWhileConnected (line 92) | func holdWhileConnected(ctx context.Context) error {
  function tryExtendBreakpoint (line 145) | func tryExtendBreakpoint(ctx context.Context, currentExpiration time.Tim...
  function stopBreakpoint (line 158) | func stopBreakpoint(ctx context.Context) error {

FILE: cmd/breakpoint/main.go
  function main (line 19) | func main() {

FILE: cmd/breakpoint/resume.go
  function newResumeCmd (line 11) | func newResumeCmd() *cobra.Command {
  function init (line 36) | func init() {

FILE: cmd/breakpoint/start.go
  function init (line 19) | func init() {
  function newStartCmd (line 23) | func newStartCmd() *cobra.Command {
  function waitForReady (line 66) | func waitForReady(ctx context.Context, timeoutDuration time.Duration) (*...
  function getStatus (line 93) | func getStatus(ctx context.Context) (*v1.StatusResponse, error) {

FILE: cmd/breakpoint/status.go
  function init (line 13) | func init() {
  function newStatusCmd (line 17) | func newStatusCmd() *cobra.Command {

FILE: cmd/breakpoint/wait.go
  function init (line 22) | func init() {
  function newWaitCmd (line 26) | func newWaitCmd() *cobra.Command {
  function cancelIsOK (line 118) | func cancelIsOK(err error) error {
  type dummyAddr (line 126) | type dummyAddr struct
    method Network (line 128) | func (dummyAddr) Network() string { return "internal" }
    method String (line 129) | func (dummyAddr) String() string  { return "quic-revproxy" }

FILE: cmd/rendezvous/main.go
  type frontendConfig (line 33) | type frontendConfig struct
  function main (line 40) | func main() {
  function flagOrEnv (line 68) | func flagOrEnv(env, flag string) string {
  function flagOrEnvBool (line 76) | func flagOrEnvBool(env string, flag bool) bool {
  type Config (line 80) | type Config struct
  function run (line 90) | func run(opts Config) error {
  function makeFrontend (line 157) | func makeFrontend(fcfg frontendConfig, pub string) quicproxy.ProxyFronte...

FILE: pkg/bcontrol/client.go
  function SocketPath (line 15) | func SocketPath() (string, error) {
  function Connect (line 24) | func Connect(ctx context.Context) (pb.ControlServiceClient, *grpc.Client...

FILE: pkg/bgrpc/bgrpc.go
  function DialContext (line 11) | func DialContext(ctx context.Context, target string, opts ...grpc.DialOp...
  function clientInterceptors (line 22) | func clientInterceptors() ([]grpc.UnaryClientInterceptor, []grpc.StreamC...

FILE: pkg/blog/blog.go
  function init (line 10) | func init() {
  function New (line 14) | func New() zerolog.Logger {

FILE: pkg/config/config.go
  function LoadConfig (line 20) | func LoadConfig(ctx context.Context, file string) (ParsedConfig, error) {
  type ParsedConfig (line 102) | type ParsedConfig struct

FILE: pkg/execbackground/bg_unix.go
  function SetCreateSession (line 10) | func SetCreateSession(cmd *exec.Cmd) {

FILE: pkg/execbackground/bg_windows.go
  function SetCreateSession (line 7) | func SetCreateSession(cmd *exec.Cmd) {

FILE: pkg/github/sshkeys.go
  function ResolveSSHKeys (line 14) | func ResolveSSHKeys(ctx context.Context, usernames []string) (map[string...
  function fetchKeys (line 39) | func fetchKeys(username string) ([]string, error) {

FILE: pkg/githuboidc/claims.go
  type Claims (line 5) | type Claims struct

FILE: pkg/githuboidc/gh.go
  constant userAgent (line 18) | userAgent = "actions/oidc-client"
  type Token (line 21) | type Token struct
  function OIDCAvailable (line 25) | func OIDCAvailable() bool {
  function JWT (line 30) | func JWT(ctx context.Context, audience string) (*Token, error) {
  function oidcConf (line 68) | func oidcConf() (string, string) {

FILE: pkg/githuboidc/verifier.go
  constant githubJWKSURL (line 15) | githubJWKSURL = "https://token.actions.githubusercontent.com/.well-known...
  function ProvideVerifier (line 18) | func ProvideVerifier(ctx context.Context) (*keyfunc.JWKS, error) {
  function Validate (line 33) | func Validate(ctx context.Context, jwks *keyfunc.JWKS, tokenStr string) ...

FILE: pkg/httperrors/httperrors.go
  type HttpError (line 9) | type HttpError struct
    method Error (line 14) | func (he HttpError) Error() string {
  function MaybeError (line 22) | func MaybeError(resp *http.Response) error {

FILE: pkg/internalserver/internalserver.go
  type waiterService (line 19) | type waiterService struct
    method Extend (line 66) | func (g waiterService) Extend(ctx context.Context, req *pb.ExtendReque...
    method Status (line 73) | func (g waiterService) Status(ctx context.Context, req *emptypb.Empty)...
    method Resume (line 82) | func (g waiterService) Resume(ctx context.Context, req *emptypb.Empty)...
  function ListenAndServe (line 24) | func ListenAndServe(ctx context.Context, mgr *waiter.Manager) error {

FILE: pkg/jsonfile/load.go
  function Load (line 8) | func Load(filename string, target any) error {

FILE: pkg/passthrough/listener.go
  type Listener (line 11) | type Listener struct
    method Accept (line 22) | func (pl Listener) Accept() (net.Conn, error) {
    method Addr (line 35) | func (pl Listener) Addr() net.Addr {
    method Close (line 39) | func (pl Listener) Close() error {
    method Offer (line 48) | func (pl Listener) Offer(conn net.Conn) error {
  function NewListener (line 18) | func NewListener(ctx context.Context, addr net.Addr) Listener {

FILE: pkg/quicgrpc/grpccreds.go
  type QuicCreds (line 12) | type QuicCreds struct
    method ClientHandshake (line 18) | func (m QuicCreds) ClientHandshake(ctx context.Context, addr string, c...
    method ServerHandshake (line 22) | func (m QuicCreds) ServerHandshake(conn net.Conn) (net.Conn, credentia...
    method Info (line 30) | func (m QuicCreds) Info() credentials.ProtocolInfo {
    method Clone (line 34) | func (m QuicCreds) Clone() credentials.TransportCredentials {
    method OverrideServerName (line 38) | func (m QuicCreds) OverrideServerName(string) error {
  type QuicAuthInfo (line 42) | type QuicAuthInfo struct
    method AuthType (line 47) | func (QuicAuthInfo) AuthType() string {

FILE: pkg/quicnet/conn.go
  type Conn (line 10) | type Conn struct
    method LocalAddr (line 15) | func (cw Conn) LocalAddr() net.Addr {
    method RemoteAddr (line 19) | func (cw Conn) RemoteAddr() net.Addr {
  function OpenStream (line 23) | func OpenStream(ctx context.Context, conn quic.Connection) (Conn, error) {

FILE: pkg/quicnet/listener.go
  type Listener (line 19) | type Listener struct
    method loop (line 36) | func (l *Listener) loop() {
    method closeWithErr (line 48) | func (l *Listener) closeWithErr(err error) error {
    method waitForStream (line 72) | func (l *Listener) waitForStream(conn quic.Connection) {
    method queue (line 88) | func (l *Listener) queue(conn quic.Connection, stream quic.Stream) {
    method Accept (line 104) | func (l *Listener) Accept() (net.Conn, error) {
    method Close (line 125) | func (l *Listener) Close() error {
    method Addr (line 129) | func (l *Listener) Addr() net.Addr {
  function NewListener (line 29) | func NewListener(ctx context.Context, l quic.Listener) *Listener {

FILE: pkg/quicproxy/proxyproto.go
  type ProxyProtoFrontend (line 15) | type ProxyProtoFrontend struct
    method ListenAndServe (line 24) | func (pf *ProxyProtoFrontend) ListenAndServe(ctx context.Context) error {
    method allocate (line 74) | func (pf *ProxyProtoFrontend) allocate(ctx context.Context, handler fu...
    method Handle (line 97) | func (pf *ProxyProtoFrontend) Handle(ctx context.Context, handlers Han...

FILE: pkg/quicproxy/rawproto.go
  type RawFrontend (line 11) | type RawFrontend struct
    method ListenAndServe (line 15) | func (rf RawFrontend) ListenAndServe(ctx context.Context) error {
    method Handle (line 19) | func (rf RawFrontend) Handle(ctx context.Context, handlers Handlers) e...

FILE: pkg/quicproxy/serve.go
  type Allocation (line 15) | type Allocation struct
  type ProxyFrontend (line 19) | type ProxyFrontend interface
  type Handlers (line 24) | type Handlers struct
  function ServeProxy (line 30) | func ServeProxy(ctx context.Context, frontend ProxyFrontend, conn quic.C...
  function cancelIsOK (line 50) | func cancelIsOK(err error) error {

FILE: pkg/quicproxy/service.go
  type Server (line 27) | type Server struct
    method Close (line 79) | func (srv *Server) Close() error {
    method Serve (line 83) | func (srv *Server) Serve(ctx context.Context) error {
  type ServerOpts (line 33) | type ServerOpts struct
  function NewServer (line 40) | func NewServer(ctx context.Context, opts ServerOpts) (*Server, error) {
  type server (line 95) | type server struct
    method Register (line 106) | func (srv server) Register(req *apipb.RegisterRequest, server apipb.Pr...
  function validateGitHubOIDC (line 132) | func validateGitHubOIDC(ctx context.Context, logger zerolog.Logger, jwks...

FILE: pkg/quicproxyclient/client.go
  type Handlers (line 27) | type Handlers struct
  function Serve (line 32) | func Serve(ctx context.Context, endpoint string, md metadata.MD, handler...

FILE: pkg/sshd/keepalive.go
  function keepAlive (line 13) | func keepAlive(ctx context.Context, logger zerolog.Logger, session ssh.S...

FILE: pkg/sshd/pty_unix.go
  function handlePty (line 17) | func handlePty(session io.ReadWriter, ptyReq ssh.Pty, winCh <-chan ssh.W...
  function syncWinSize (line 35) | func syncWinSize(ptyFile *os.File, winCh <-chan ssh.Window) {
  function setWinsize (line 41) | func setWinsize(f *os.File, w, h int) {

FILE: pkg/sshd/pty_windows.go
  function handlePty (line 13) | func handlePty(session io.ReadWriter, ptyReq ssh.Pty, winCh <-chan ssh.W...

FILE: pkg/sshd/sftp.go
  function makeSftpHandler (line 11) | func makeSftpHandler(logger zerolog.Logger) ssh.SubsystemHandler {

FILE: pkg/sshd/sshd.go
  type SSHServerOpts (line 22) | type SSHServerOpts struct
  type sshKey (line 32) | type sshKey struct
  type SSHServer (line 37) | type SSHServer struct
  function MakeServer (line 42) | func MakeServer(ctx context.Context, opts SSHServerOpts) (*SSHServer, er...
  function lookupKey (line 177) | func lookupKey(allowed []sshKey, key ssh.PublicKey) (sshKey, bool) {

FILE: pkg/tlscerts/tlscerts.go
  type Subjects (line 14) | type Subjects struct
  function GenerateECDSAPair (line 19) | func GenerateECDSAPair(subjects Subjects, duration time.Duration) ([]byt...
  function newSerialNumber (line 57) | func newSerialNumber() (*big.Int, error) {

FILE: pkg/waiter/output.go
  function PrintConnectionInfo (line 14) | func PrintConnectionInfo(endpoint string, deadline time.Time, output io....

FILE: pkg/waiter/slackbot.go
  type botInstance (line 18) | type botInstance struct
    method Close (line 48) | func (b *botInstance) Close() error {
    method makeBlocks (line 55) | func (b *botInstance) makeBlocks(leaving bool) slack.MsgOption {
    method sendUpdate (line 63) | func (b *botInstance) sendUpdate(ctx context.Context, leaving bool) er...
    method loop (line 68) | func (b *botInstance) loop(ctx context.Context) error {
  function startBot (line 27) | func startBot(ctx context.Context, m *Manager, conf v1.SlackBot) *botIns...
  type renderGitHubProps (line 85) | type renderGitHubProps struct
  function prepareGitHubProps (line 95) | func prepareGitHubProps(ctx context.Context) renderGitHubProps {
  function renderGitHubMessage (line 117) | func renderGitHubMessage(props renderGitHubProps, endpoint string, exp t...
  function maybeCommitURL (line 164) | func maybeCommitURL(repo string, event github.PushEvent) string {

FILE: pkg/waiter/template.go
  function execTemplate (line 7) | func execTemplate(value any, mapping func(string) string) any {
  function execMapTemplate (line 32) | func execMapTemplate(input map[string]any, mapping func(string) string) ...

FILE: pkg/waiter/template_test.go
  function TestExecTemplate (line 11) | func TestExecTemplate(t *testing.T) {

FILE: pkg/waiter/waiter.go
  constant logTickInterval (line 20) | logTickInterval = 1 * time.Minute
  constant Stamp (line 22) | Stamp = time.Stamp + " MST"
  type ManagerOpts (line 25) | type ManagerOpts struct
  type ManagerStatus (line 32) | type ManagerStatus struct
  type Manager (line 38) | type Manager struct
    method Wait (line 84) | func (m *Manager) Wait() error {
    method loop (line 89) | func (m *Manager) loop(ctx context.Context) {
    method ExtendWait (line 133) | func (m *Manager) ExtendWait(dur time.Duration) time.Time {
    method StopWait (line 148) | func (m *Manager) StopWait() {
    method Expiration (line 153) | func (m *Manager) Expiration() time.Time {
    method Endpoint (line 159) | func (m *Manager) Endpoint() string {
    method Status (line 165) | func (m *Manager) Status() ManagerStatus {
    method SetEndpoint (line 175) | func (m *Manager) SetEndpoint(addr string) {
    method SetConnectionCountCallback (line 210) | func (m *Manager) SetConnectionCountCallback(callback func() uint32) {
    method announce (line 241) | func (m *Manager) announce() {
  function NewManager (line 52) | func NewManager(ctx context.Context, opts ManagerOpts) (*Manager, contex...
  function logTick (line 124) | func logTick() time.Duration {
  function expand (line 216) | func expand(addr string, exp time.Time) func(key string) string {
  function nchars (line 246) | func nchars(ch rune, n int) string {

FILE: pkg/webhook/notifier.go
  constant userAgent (line 13) | userAgent = "Breakpoint/1.0"
  function Notify (line 16) | func Notify(ctx context.Context, endpoint string, payload any) error {
Condensed preview — 71 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (185K chars).
[
  {
    "path": ".dockerignore",
    "chars": 8,
    "preview": "fly.toml"
  },
  {
    "path": ".github/slack-notification.json",
    "chars": 1318,
    "preview": "{\n  \"url\": \"${SLACK_WEBHOOK_URL}\",\n  \"payload\": {\n    \"blocks\": [\n      {\n        \"type\": \"header\",\n        \"text\": {\n  "
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 1257,
    "preview": "name: Build\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n\npermissions:\n  contents: read # Checkout the co"
  },
  {
    "path": ".github/workflows/checks.yml",
    "chars": 987,
    "preview": "name: Commit Checks\non:\n  pull_request:\n    branches:\n      - \"*\"\n  push:\n    branches:\n      - main\n  workflow_dispatch"
  },
  {
    "path": ".github/workflows/goreleaser.yml",
    "chars": 462,
    "preview": "name: Release Binaries\n\non:\n  push:\n    tags: [\"v*\"]\n\njobs:\n  goreleaser:\n    runs-on: nscloud-ubuntu-24.04-amd64-4x8\n  "
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 1405,
    "preview": "name: Release Rendezvous Docker image\n\non:\n  release:\n    types: [released]\n\npermissions:\n  contents: read # Checkout th"
  },
  {
    "path": ".gitignore",
    "chars": 485,
    "preview": "# If you prefer the allow list template instead of the deny list, see community template:\n# https://github.com/github/gi"
  },
  {
    "path": ".goreleaser.yaml",
    "chars": 735,
    "preview": "before:\n  hooks:\n    # You may remove this if you don't use go modules.\n    - go mod tidy\nbuilds:\n  - id: breakpoint\n   "
  },
  {
    "path": "Dockerfile",
    "chars": 244,
    "preview": "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 bu"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 11482,
    "preview": "<img src=\"https://raw.githubusercontent.com/namespacelabs/breakpoint/main/docs/imgs/breakpoint-banner.png\" alt=\"Breakpoi"
  },
  {
    "path": "api/private/v1/configtype.go",
    "chars": 718,
    "preview": "package v1\n\ntype WaitConfig struct {\n\tEndpoint              string    `json:\"endpoint\"`\n\tDuration              string   "
  },
  {
    "path": "api/private/v1/service.pb.go",
    "chars": 13930,
    "preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.29.1\n// \tprotoc        (unknown)\n// s"
  },
  {
    "path": "api/private/v1/service.proto",
    "chars": 786,
    "preview": "syntax = \"proto3\";\n\npackage namespacelabs.breakpoint.private;\n\nimport \"google/protobuf/duration.proto\";\nimport \"google/p"
  },
  {
    "path": "api/private/v1/service_grpc.pb.go",
    "chars": 6877,
    "preview": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.3.0\n// - protoc           "
  },
  {
    "path": "api/public/v1/service.pb.go",
    "chars": 7492,
    "preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.29.1\n// \tprotoc        (unknown)\n// s"
  },
  {
    "path": "api/public/v1/service.proto",
    "chars": 436,
    "preview": "syntax = \"proto3\";\n\npackage namespacelabs.breakpoint;\n\noption go_package = \"namespacelabs.dev/breakpoint/api/public/v1\";"
  },
  {
    "path": "api/public/v1/service_grpc.pb.go",
    "chars": 4518,
    "preview": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.3.0\n// - protoc           "
  },
  {
    "path": "api/public/v1/types.go",
    "chars": 166,
    "preview": "package v1\n\nconst (\n\tQuicProto = \"breakpoint-grpc\"\n\n\tGitHubOIDCTokenHeader = \"x-breakpoint-github-oidc-token\"\n\n\tGitHubOI"
  },
  {
    "path": "buf.gen.yaml",
    "chars": 140,
    "preview": "version: v1\nplugins:\n  - plugin: go\n    out: .\n    opt: paths=source_relative\n  - plugin: go-grpc\n    out: .\n    opt: pa"
  },
  {
    "path": "cmd/breakpoint/attach.go",
    "chars": 1094,
    "preview": "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\"namesp"
  },
  {
    "path": "cmd/breakpoint/extend.go",
    "chars": 1384,
    "preview": "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\tp"
  },
  {
    "path": "cmd/breakpoint/hold.go",
    "chars": 4126,
    "preview": "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\"goo"
  },
  {
    "path": "cmd/breakpoint/main.go",
    "chars": 603,
    "preview": "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/br"
  },
  {
    "path": "cmd/breakpoint/resume.go",
    "chars": 686,
    "preview": "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\"namespacela"
  },
  {
    "path": "cmd/breakpoint/start.go",
    "chars": 2265,
    "preview": "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.o"
  },
  {
    "path": "cmd/breakpoint/status.go",
    "chars": 1164,
    "preview": "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\"names"
  },
  {
    "path": "cmd/breakpoint/wait.go",
    "chars": 2933,
    "preview": "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/ref"
  },
  {
    "path": "cmd/rendezvous/main.go",
    "chars": 4333,
    "preview": "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\""
  },
  {
    "path": "docs/CONTRIBUTING.md",
    "chars": 2485,
    "preview": "# Contributing to Breakpoint\n\n## Where to Start\n\nYou can find good issues to tackle with labels [`good first issue`](htt"
  },
  {
    "path": "docs/server-setup.md",
    "chars": 906,
    "preview": "# Rendezvous Server Setup\n\nThe `rendezvous` source code is 100% open-source and you can self-host it wherever you want.\n"
  },
  {
    "path": "examples/wait.withslack.json",
    "chars": 1542,
    "preview": "{\n  \"webhooks\": [\n    {\n      \"url\": \"${SLACK_WEBHOOK_URL}\",\n      \"payload\": {\n        \"blocks\": [\n          {\n        "
  },
  {
    "path": "flake.nix",
    "chars": 536,
    "preview": "{\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  out"
  },
  {
    "path": "fly.toml",
    "chars": 883,
    "preview": "[build]\ndockerfile = \"Dockerfile\"\n\n[env]\nPROXY_LISTEN = \"fly-global-services:5000\"\nPROXY_PUBLIC = \"rendezvous.namespace."
  },
  {
    "path": "go.mod",
    "chars": 2611,
    "preview": "module namespacelabs.dev/breakpoint\n\ngo 1.20\n\nrequire (\n\tgithub.com/MicahParks/keyfunc v1.9.0\n\tgithub.com/creack/pty v1."
  },
  {
    "path": "go.sum",
    "chars": 26018,
    "preview": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ngithub.com/BurntSushi/toml v0.3.1/go."
  },
  {
    "path": "pkg/README.md",
    "chars": 29,
    "preview": "Main components and packages."
  },
  {
    "path": "pkg/bcontrol/client.go",
    "chars": 936,
    "preview": "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/"
  },
  {
    "path": "pkg/bgrpc/bgrpc.go",
    "chars": 830,
    "preview": "package bgrpc\n\nimport (\n\t\"context\"\n\n\tgrpc_middleware \"github.com/grpc-ecosystem/go-grpc-middleware\"\n\tgrpc_prometheus \"gi"
  },
  {
    "path": "pkg/blog/blog.go",
    "chars": 302,
    "preview": "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.RFC3339N"
  },
  {
    "path": "pkg/config/config.go",
    "chars": 2277,
    "preview": "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."
  },
  {
    "path": "pkg/execbackground/bg_unix.go",
    "chars": 180,
    "preview": "//go:build !windows\n\npackage execbackground\n\nimport (\n\t\"os/exec\"\n\t\"syscall\"\n)\n\nfunc SetCreateSession(cmd *exec.Cmd) {\n\tc"
  },
  {
    "path": "pkg/execbackground/bg_windows.go",
    "chars": 127,
    "preview": "//go:build windows\n\npackage execbackground\n\nimport \"os/exec\"\n\nfunc SetCreateSession(cmd *exec.Cmd) {\n\tpanic(\"not support"
  },
  {
    "path": "pkg/github/sshkeys.go",
    "chars": 1404,
    "preview": "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 Resolv"
  },
  {
    "path": "pkg/githuboidc/claims.go",
    "chars": 818,
    "preview": "package githuboidc\n\nimport \"github.com/golang-jwt/jwt/v4\"\n\ntype Claims struct {\n\tjwt.RegisteredClaims\n\n\tJobWorkflowRef  "
  },
  {
    "path": "pkg/githuboidc/gh.go",
    "chars": 1692,
    "preview": "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."
  },
  {
    "path": "pkg/githuboidc/verifier.go",
    "chars": 1039,
    "preview": "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-j"
  },
  {
    "path": "pkg/httperrors/httperrors.go",
    "chars": 617,
    "preview": "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"
  },
  {
    "path": "pkg/internalserver/internalserver.go",
    "chars": 1951,
    "preview": "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"
  },
  {
    "path": "pkg/jsonfile/load.go",
    "chars": 207,
    "preview": "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(f"
  },
  {
    "path": "pkg/passthrough/listener.go",
    "chars": 924,
    "preview": "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    conte"
  },
  {
    "path": "pkg/quicgrpc/grpccreds.go",
    "chars": 1156,
    "preview": "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\"name"
  },
  {
    "path": "pkg/quicnet/conn.go",
    "chars": 482,
    "preview": "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"
  },
  {
    "path": "pkg/quicnet/listener.go",
    "chars": 2426,
    "preview": "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/zer"
  },
  {
    "path": "pkg/quicproxy/proxyproto.go",
    "chars": 2514,
    "preview": "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-pr"
  },
  {
    "path": "pkg/quicproxy/rawproto.go",
    "chars": 1258,
    "preview": "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 "
  },
  {
    "path": "pkg/quicproxy/serve.go",
    "chars": 1343,
    "preview": "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"
  },
  {
    "path": "pkg/quicproxy/service.go",
    "chars": 4415,
    "preview": "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/qu"
  },
  {
    "path": "pkg/quicproxyclient/client.go",
    "chars": 2249,
    "preview": "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-pr"
  },
  {
    "path": "pkg/sshd/keepalive.go",
    "chars": 642,
    "preview": "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"
  },
  {
    "path": "pkg/sshd/pty_unix.go",
    "chars": 902,
    "preview": "//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\""
  },
  {
    "path": "pkg/sshd/pty_windows.go",
    "chars": 258,
    "preview": "//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(se"
  },
  {
    "path": "pkg/sshd/sftp.go",
    "chars": 564,
    "preview": "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 makeS"
  },
  {
    "path": "pkg/sshd/sshd.go",
    "chars": 4684,
    "preview": "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\"gith"
  },
  {
    "path": "pkg/tlscerts/tlscerts.go",
    "chars": 1283,
    "preview": "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"
  },
  {
    "path": "pkg/waiter/output.go",
    "chars": 1004,
    "preview": "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/re"
  },
  {
    "path": "pkg/waiter/slackbot.go",
    "chars": 4742,
    "preview": "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"
  },
  {
    "path": "pkg/waiter/template.go",
    "chars": 660,
    "preview": "package waiter\n\nimport (\n\t\"os\"\n)\n\nfunc execTemplate(value any, mapping func(string) string) any {\n\tif value == nil {\n\t\tr"
  },
  {
    "path": "pkg/waiter/template_test.go",
    "chars": 1510,
    "preview": "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/"
  },
  {
    "path": "pkg/waiter/waiter.go",
    "chars": 4981,
    "preview": "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-humani"
  },
  {
    "path": "pkg/webhook/notifier.go",
    "chars": 669,
    "preview": "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/httperror"
  }
]

About this extraction

This page contains the full source code of the namespacelabs/breakpoint GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 71 files (165.5 KB), approximately 57.8k tokens, and a symbol index with 253 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!