diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml index 97b124ba1..441e92ab0 100644 --- a/.github/workflows/release-docker.yml +++ b/.github/workflows/release-docker.yml @@ -6,18 +6,18 @@ name: Docker Build and Push # documentation. on: + workflow_dispatch: push: - # branches: [ "main" ] - # Publish semver tags as releases. + branches: [ "master" ] tags: [ 'v*.*.*' ] paths-ignore: - 'docs/**' + - '*.md' + - 'pages/**' env: - # Use docker.io for Docker Hub if empty - REGISTRY: ghcr.io - # github.repository as / - #IMAGE_NAME: ${{ github.repository }} + REGISTRY: docker.io + IMAGE_NAME: mrlyc/copilot-api jobs: @@ -56,13 +56,13 @@ jobs: # Login against a Docker registry except on PR # https://github.com/docker/login-action - - name: Log into registry ${{ env.REGISTRY }} + - name: Log into Docker Hub if: github.event_name != 'pull_request' uses: docker/login-action@v2 with: registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} # Extract metadata (tags, labels) for Docker # https://github.com/docker/metadata-action @@ -71,11 +71,13 @@ jobs: uses: docker/metadata-action@v4 with: github-token: ${{ secrets.GITHUB_TOKEN }} - images: ${{ env.REGISTRY }}/${{ github.repository }} + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | + type=raw,value=latest,enable={{is_default_branch}} type=semver,pattern=v{{version}} type=semver,pattern=v{{major}}.{{minor}} type=semver,pattern=v{{major}} + type=sha,prefix= # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action diff --git a/README.md b/README.md index 0d36c13c9..82d17c868 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,29 @@ bun install ## Using with Docker -Build image +### Using Pre-built Image + +You can use the pre-built Docker image from Docker Hub: + +```sh +# Pull the latest image +docker pull mrlyc/copilot-api:latest + +# Run the container +docker run -p 4141:4141 -v $(pwd)/copilot-data:/root/.local/share/copilot-api mrlyc/copilot-api:latest + +# Run with environment variables +docker run -p 4141:4141 -e GH_TOKEN=your_token -e API_KEY=your_secret_key mrlyc/copilot-api:latest +``` + +Available tags: +- `latest` - Latest build from main branch +- `v0.7.0` - Specific version +- `v0.7` - Latest patch of v0.7.x +- `v0` - Latest minor/patch of v0.x.x +- `` - Specific commit + +### Building from Source ```sh docker build -t copilot-api . @@ -91,6 +113,9 @@ docker build --build-arg GH_TOKEN=your_github_token_here -t copilot-api . # Run with GitHub token docker run -p 4141:4141 -e GH_TOKEN=your_github_token_here copilot-api +# Run with API key for authentication (requires Bearer token in requests) +docker run -p 4141:4141 -e GH_TOKEN=your_token -e API_KEY=your_secret_key copilot-api + # Run with additional options docker run -p 4141:4141 -e GH_TOKEN=your_token copilot-api start --verbose --port 4141 ``` @@ -106,6 +131,7 @@ services: - "4141:4141" environment: - GH_TOKEN=your_github_token_here + - API_KEY=your_secret_api_key # Optional: enables Bearer token authentication restart: unless-stopped ``` @@ -163,6 +189,7 @@ The following command line options are available for the `start` command: | --claude-code | Generate a command to launch Claude Code with Copilot API config | false | -c | | --show-token | Show GitHub and Copilot tokens on fetch and refresh | false | none | | --proxy-env | Initialize proxy from environment variables | false | none | +| --api-key | API key for Bearer token authentication | none | -k | ### Auth Command Options @@ -255,6 +282,28 @@ npx copilot-api@latest debug --json # Initialize proxy from environment variables (HTTP_PROXY, HTTPS_PROXY, etc.) npx copilot-api@latest start --proxy-env + +# Enable API key authentication (clients must send Bearer token) +npx copilot-api@latest start --api-key your_secret_key +``` + +## API Key Authentication + +You can secure your Copilot API proxy with Bearer token authentication using the `--api-key` option (or `API_KEY` environment variable in Docker). + +When enabled, all API requests must include an `Authorization` header: + +``` +Authorization: Bearer your_secret_key +``` + +Example with curl: + +```sh +curl -X POST http://localhost:4141/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your_secret_key" \ + -d '{"model": "gpt-4.1", "messages": [{"role": "user", "content": "Hello"}]}' ``` ## Using the Usage Viewer diff --git a/entrypoint.sh b/entrypoint.sh index dfe63c902..6d2df1adf 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -3,7 +3,19 @@ if [ "$1" = "--auth" ]; then # Run auth command exec bun run dist/main.js auth else - # Default command - exec bun run dist/main.js start -g "$GH_TOKEN" "$@" + # Build command with optional arguments + CMD="bun run dist/main.js start" + + if [ -n "$GH_TOKEN" ]; then + CMD="$CMD -g $GH_TOKEN" + fi + + if [ -n "$API_KEY" ]; then + CMD="$CMD --api-key $API_KEY" + fi + + # Execute with any additional arguments + exec $CMD "$@" fi + diff --git a/src/lib/auth-middleware.ts b/src/lib/auth-middleware.ts new file mode 100644 index 000000000..a21af5e86 --- /dev/null +++ b/src/lib/auth-middleware.ts @@ -0,0 +1,28 @@ +import type { Context, Next } from "hono" + +import { state } from "./state" + +export async function authMiddleware( + c: Context, + next: Next, +): Promise { + if (!state.apiKey) { + await next() + return c.res + } + + const authHeader = c.req.header("Authorization") + + if (!authHeader) { + return c.json({ error: "Missing Authorization header" }, 401) + } + + const token = authHeader.replace(/^Bearer\s+/i, "") + + if (token !== state.apiKey) { + return c.json({ error: "Invalid API key" }, 401) + } + + await next() + return c.res +} diff --git a/src/lib/state.ts b/src/lib/state.ts index 5ba4dc1d1..23db928b5 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -15,6 +15,9 @@ export interface State { // Rate limiting configuration rateLimitSeconds?: number lastRequestTimestamp?: number + + // API key for authentication + apiKey?: string } export const state: State = { diff --git a/src/server.ts b/src/server.ts index 462a278f3..744ddc7b2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,6 +2,7 @@ import { Hono } from "hono" import { cors } from "hono/cors" import { logger } from "hono/logger" +import { authMiddleware } from "./lib/auth-middleware" import { completionRoutes } from "./routes/chat-completions/route" import { embeddingRoutes } from "./routes/embeddings/route" import { messageRoutes } from "./routes/messages/route" @@ -13,6 +14,7 @@ export const server = new Hono() server.use(logger()) server.use(cors()) +server.use(authMiddleware) server.get("/", (c) => c.text("Server running")) diff --git a/src/start.ts b/src/start.ts index 14abbbdff..50d05f9fc 100644 --- a/src/start.ts +++ b/src/start.ts @@ -25,6 +25,7 @@ interface RunServerOptions { claudeCode: boolean showToken: boolean proxyEnv: boolean + apiKey?: string } export async function runServer(options: RunServerOptions): Promise { @@ -46,6 +47,7 @@ export async function runServer(options: RunServerOptions): Promise { state.rateLimitSeconds = options.rateLimit state.rateLimitWait = options.rateLimitWait state.showToken = options.showToken + state.apiKey = options.apiKey await ensurePaths() await cacheVSCodeVersion() @@ -184,6 +186,11 @@ export const start = defineCommand({ default: false, description: "Initialize proxy from environment variables", }, + "api-key": { + alias: "k", + type: "string", + description: "API key for Bearer token authentication", + }, }, run({ args }) { const rateLimitRaw = args["rate-limit"] @@ -202,6 +209,7 @@ export const start = defineCommand({ claudeCode: args["claude-code"], showToken: args["show-token"], proxyEnv: args["proxy-env"], + apiKey: args["api-key"], }) }, })