Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
04f50e1
fix: harden repository data access and profile visibility
lantos1618 Oct 19, 2026
99037c7
Initialize Resend only when sending email
lantos1618 Oct 19, 2026
aeee040
Prompt to replace unreadable BYOK keys
lantos1618 Oct 19, 2026
41eb283
Retry Stripe webhooks when configuration is missing
lantos1618 Oct 19, 2026
287241b
Fix Gemini usage cost estimates and preserve usage dates
lantos1618 Oct 19, 2026
a38dd50
fix(billing): prevent duplicate subscriptions at checkout
lantos1618 Oct 19, 2026
5116430
Sync Stripe entitlements from current subscription state
lantos1618 Oct 19, 2026
fa4a688
fix(ai): route selected Gemini keys per request and attribute model u…
lantos1618 Oct 19, 2026
eef0746
fix(github): preserve app link and retry failed webhook enqueue
lantos1618 Oct 19, 2026
e231ce7
fix(billing): expire AI entitlement at subscription period end
lantos1618 Oct 19, 2026
5ea5d8c
Add read-only monthly Pro token quota check
lantos1618 Oct 19, 2026
9fcd79d
Exclude BYOK usage from platform AI costs
lantos1618 Oct 19, 2026
f7e0f8e
feat(ai): upgrade stable Gemini models with BYOK wiki fallback
lantos1618 Oct 19, 2026
349f7c7
Make GitHub installation event retries idempotent
lantos1618 Oct 19, 2026
18890ba
Prepare disabled Vercel webhook daemon and shared rate limit
lantos1618 Oct 19, 2026
541bc7f
Align homepage promises with available product features
lantos1618 Oct 19, 2026
718f842
fix(analysis): claim SSE jobs once across instances
lantos1618 Oct 19, 2026
286adb2
Enforce monthly Pro token quota before paid AI generation
lantos1618 Oct 19, 2026
9a9692b
Hide GitHub App install calls until review worker is active
lantos1618 Oct 19, 2026
25b1764
Normalize GitHub App PEM for installation token auth
lantos1618 Oct 19, 2026
b27b80c
Price Gemini embedding-001 usage correctly
lantos1618 Oct 19, 2026
3ffdd41
Describe paid review allowance and current model pricing accurately
lantos1618 Oct 19, 2026
569d520
Use an existing image and canonical base for social previews
lantos1618 Oct 19, 2026
59e0d66
Enforce shared paid AI rate limit before generation
lantos1618 Oct 19, 2026
a6aa6a5
Count Gemini reasoning tokens in usage and quota
lantos1618 Oct 19, 2026
01841b4
Bound public semantic search embedding and visibility costs
lantos1618 Oct 19, 2026
17c56a1
Isolate paid AI limiter logic for combined tests
lantos1618 Oct 19, 2026
53772d7
fix(hire): gate paid matching and meter selected Gemini key
lantos1618 Oct 19, 2026
c991290
Align pricing copy and bound hiring input
lantos1618 Oct 19, 2026
a8715b2
Gate GitHub App setup UI while reviews are paused
lantos1618 Oct 19, 2026
c9084ad
Apply actor search budgets before shared budgets
lantos1618 Oct 19, 2026
005efff
Hide unavailable GitHub App upgrades
lantos1618 Oct 19, 2026
dc6efbb
Reserve stargazer free uses atomically and stop profile emails
lantos1618 Oct 19, 2026
452736e
Clarify paused private access and BYOK limits in settings
lantos1618 Oct 19, 2026
166f1ee
fix(billing): reserve one checkout session per user
lantos1618 Oct 19, 2026
d018cf4
Clarify paid hire access and preview controls
lantos1618 Oct 19, 2026
99a20ba
Run all release security regressions in CI
lantos1618 Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.git
.vercel
.next
node_modules
.env
.env.*
tests
public
*.md
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ DEBUG="false"
# Set to "false" to use GitHub OAuth in development
NEXT_PUBLIC_USE_DEV_AUTH="true"

# Optional: local Playwright session endpoint. Set both values only for E2E tests.
# Generate TEST_SESSION_SECRET with: openssl rand -hex 32
# ENABLE_TEST_SESSION="true"
# TEST_SESSION_SECRET=""

# =============================================================================
# Dev Environment System Configuration
# =============================================================================
Expand Down Expand Up @@ -87,3 +92,9 @@ TERRAFORM_WORKSPACE_ID="your-workspace-id"
# in UPSTASH_REDIS_REST_URL, which is incompatible with BullMQ.
# See WORKER_DEPLOYMENT.md for context.
REDIS_URL="redis://default:password@host:6379"

# Required by the public feature-request rate limiter. Use an Upstash REST
# URL/token pair (or KV_REST_API_URL + KV_REST_API_TOKEN); REDIS_URL alone is
# for the webhook queue and cannot serve this HTTP API.
UPSTASH_REDIS_REST_URL="https://example.upstash.io"
UPSTASH_REDIS_REST_TOKEN="replace-with-upstash-rest-token"
57 changes: 57 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Security checks

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.5
- run: bun install --frozen-lockfile
- run: bun audit --audit-level=critical
- run: bunx tsc --noEmit
- name: Security regression tests
run: |
for file in \
tests/safe-local-path.test.ts \
tests/security-auth-profile.test.ts \
tests/profile-public-provenance.test.ts \
tests/repository-access.test.ts \
tests/public-visibility.test.ts \
tests/feature-request-security.test.ts \
tests/stripe-webhook-config.test.ts \
tests/stripe-subscription-webhook.test.ts \
tests/billing-checkout-guard.test.ts \
tests/checkout-session-reservation.test.ts \
tests/byok-key-status.test.ts \
tests/github-webhook-enqueue-retry.test.ts \
tests/sign-out-installation-link.test.ts \
tests/analysis-job-store.test.ts \
tests/gemini-key-routing.test.ts \
tests/hire-ai-routing.test.ts \
tests/hire-route-gates.test.ts \
tests/subscription-entitlement.test.ts \
tests/cost-calculator.test.ts \
tests/monthly-quota.test.ts \
tests/monthly-quota-gates.test.ts \
tests/paid-ai-rate-limit.test.ts \
tests/semantic-search-identity.test.ts \
tests/semantic-search-fallback.test.ts \
tests/stargazer-perk-access.test.ts \
tests/stargazer-reservation.test.ts \
tests/test-session-guard.test.ts \
tests/trpc-cache-control.test.ts \
tests/wiki-visibility.test.ts \
tests/wiki-byok-context.test.ts \
tests/wiki-viewers-access.test.ts; do
bun test "$file" || exit 1
done
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM oven/bun:1.3.5

WORKDIR /app

COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production

COPY tsconfig.json ./
COPY src ./src

USER bun
CMD ["bun", "src/workers/webhook-worker.ts"]
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,20 @@ bun run db:start # Start PostgreSQL with Docker
bun run db:studio # Open Drizzle Studio
bun run db:push # Push schema changes
bun test # Run tests
bun run test:e2e # Run Playwright tests (local test-session secret required)
bun run worker:webhook # Drain the GitHub webhook queue (long-lived; see WORKER_DEPLOYMENT.md)
```

> **Note**: `worker:webhook` is a long-lived process that handles GitHub webhook
> events outside the Vercel 30s request budget. It must run on Railway or a VM
> (not Vercel) — see [WORKER_DEPLOYMENT.md](./WORKER_DEPLOYMENT.md) for details.

Authenticated Playwright tests require `ENABLE_TEST_SESSION=true` and a separate
`TEST_SESSION_SECRET` of at least 32 random characters in `.env.local` (for
example, generate one with `openssl rand -hex 32`). The test-session endpoint
accepts matching secret requests only on a local development server. Never use
`BETTER_AUTH_SECRET` as the test-session secret.

### Environment Variables

See [`.env.example`](./.env.example) for all configuration options. Key variables:
Expand All @@ -232,7 +239,7 @@ GITHUB_CLIENT_SECRET=...
BETTER_AUTH_SECRET=...

# AI Features
GOOGLE_GENERATIVE_AI_API_KEY=...
GEMINI_API_KEY=...

# Payments (optional)
STRIPE_SECRET_KEY=...
Expand Down Expand Up @@ -320,9 +327,16 @@ bun start

**Important**: Run database migrations before deploying:
```bash
bun run db:push
bun run migrate
```

This security update adds `0030_artifact_privacy.sql` and
`0031_better_auth_account_password.sql`. Apply them before deploying the new
application code. On an existing database without an `app_migrations` journal,
baseline the already-deployed migrations **from the old checkout first**;
then run `bun run migrate` from this checkout so these new files are applied.
Do not baseline the new files without running them.

---

## 🗺️ Roadmap
Expand Down
139 changes: 79 additions & 60 deletions WORKER_DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -1,81 +1,100 @@
# Worker deployment
# GitHub webhook worker deployment

The webhook worker (`src/workers/webhook-worker.ts`) is a long-lived process
that drains the BullMQ `github-webhooks` queue. **Vercel cannot host it** —
Vercel functions are serverless and time out after at most 30s, which is
exactly the problem this worker exists to solve.
`src/workers/webhook-worker.ts` is a persistent BullMQ consumer for the
`github-webhooks` Redis queue. The `github.gg` Vercel project uses the repository
root as its Root Directory, so it can run this worker as a Vercel Daemon next to
the Next.js app. Its image is built from the root `Dockerfile`.

## TL;DR — recommended setup
## Current state

Run the worker on **Railway** (or any small VM you already pay for —
OVH/Hetzner/Fly.io all work the same way).
`vercel.json` declares one `webhook-worker` daemon in `iad1` with **zero
instances**. A production deployment can build its image, but will not start a
worker or consume queued jobs. Keep the count at zero until the production
credentials and queue have been checked.

```bash
bun install
bun run worker:webhook
```

Required environment variables:

| Var | Notes |
| --- | --- |
| `REDIS_URL` | ioredis TCP URL. Vercel KV exposes this; copy from `vercel env pull`. |
| `DATABASE_URL` | Same Neon URL the web app uses. |
| `GITHUB_APP_ID` | Same as web app (needed for installation token minting). |
| `GITHUB_APP_PRIVATE_KEY` | Same as web app. |
| `GOOGLE_GENERATIVE_AI_API_KEY` | For the AI analysis handlers (PR / commit / issue). |
| `ENABLE_PR_REVIEWS`, `ENABLE_COMMIT_ANALYSIS`, etc. | Feature flags must match the web app or analysis silently no-ops. |
Daemons currently run only for production deployments; preview deployments and
`vercel dev` do not run them. Vercel restarts crashed instances, collects their
stdout/stderr in project Logs (source `daemon`), and sends `SIGTERM` on rollout.
The worker closes its BullMQ connection and drains in-flight jobs on shutdown.

The worker doesn't need `GITHUB_WEBHOOK_SECRET` — signature verification
happens in the request path before the job is enqueued.
## Production prerequisites

## Why Railway
Set these in the `github.gg` Vercel project's **Production** environment:

- Native bun support (`nixpacks` autodetects `package.json`).
- One service per repo, point it at the `worker:webhook` script.
- Cheaper than Fly's smallest VM at the scale we need.
- Logs are queryable out of the box.

If you already have OVH/Hetzner credits, deploying as a systemd unit is
equivalent — no architectural difference.
| Variable | Purpose |
| --- | --- |
| `REDIS_URL` | Redis TCP connection shared by the webhook receiver and BullMQ worker. Upstash REST variables alone do not work for BullMQ. |
| `DATABASE_URL` | Neon database connection shared with the web app. |
| `GITHUB_APP_ID` and `GITHUB_PRIVATE_KEY` | GitHub App installation authentication for repository access and comments. Use the exact Vercel variable name `GITHUB_PRIVATE_KEY`. |
| `API_KEY_ENCRYPTION_SECRET` | Decrypt subscribers' saved Gemini keys for BYOK analysis. |
| `GEMINI_API_KEY` | Platform Gemini key for users without a saved BYOK key. |
| `GITHUB_WEBHOOK_SECRET` | Used by the **webhook receiver** to verify GitHub signatures before enqueueing; the worker does not read it. |

## Alternative: Vercel Cron drain endpoint
The GitHub App webhook URL must point to
`https://www.github.gg/api/webhooks/github` and use the same
`GITHUB_WEBHOOK_SECRET`. Confirm the App is installed on the intended
repositories and the receiver can enqueue a signed test delivery before
starting the worker. Keep all credentials out of Git and Docker images.

Not currently implemented. The pattern would be:
## Build and activate

1. Add `/api/cron/drain-webhooks` that pulls N jobs off the queue and processes
them inline within Vercel's 30s budget.
2. Schedule it every 30s via `vercel.json`.
To verify the image locally (with an appropriate local `.env.local`):

This avoids the extra infra but trades latency (up to 30s before a webhook is
processed) and is fragile under bursts. Only go this route if Railway/VM is
genuinely not an option.
```bash
docker build -t github-gg-webhook-worker .
docker run --rm --env-file .env.local github-gg-webhook-worker
```

## Running locally
The image runs `bun src/workers/webhook-worker.ts`; Vercel injects its
Production environment variables, so the container does not require an
`.env.local` file in production.

Once the production prerequisites pass, change
`daemons.webhook-worker.instances.iad1` from `0` to `1` and deploy to
Production, or use the project's Daemons dashboard to scale to one instance.
After a signed delivery is processed successfully, set
`NEXT_PUBLIC_GITHUB_REVIEWS_ENABLED=true` in Production and redeploy to show
the GitHub App installation calls to action on the homepage. Leave it unset
while the worker has zero instances.
Dashboard scaling creates an override that persists across deployments until
cleared; check for an override before assuming the repository value controls
the running instance count. Watch the daemon logs for `Webhook worker ready`
and confirm a new GitHub delivery leaves the BullMQ `waiting` state. Scale
back to zero if processing fails.

The daemon requests 1 GiB of memory and gives the worker 120 seconds to drain
after `SIGTERM`. Vercel Daemons are in public beta and available on all plans.
Hobby has a shared 10 Active CPU-hour monthly daemon quota; reaching it stops
the team's daemons until the next month or a Pro upgrade. Pro use is metered
for provisioned memory and active CPU. Check current Vercel Usage and the
[regional price table](https://vercel.com/docs/pricing/regional-pricing/iad1)
before scaling above zero.

## Running locally without Docker

```bash
# Terminal 1 — web app
bun run dev

# Terminal 2 — worker
bun install --frozen-lockfile
bun run worker:webhook
```

Both processes share `.env.local`. If `REDIS_URL` isn't set the worker exits
on startup with a clear error.

## Monitoring
`worker:webhook` loads `.env.local`. If `REDIS_URL` is absent, the worker exits
with a clear error.

BullMQ keeps the last 1000 completed jobs (1h retention) and 1000 failed jobs
(24h retention) — see `DEFAULT_WEBHOOK_JOB_OPTS` in `src/lib/webhook-queue.ts`.
For a UI, install [`bull-board`](https://github.com/felixmosh/bull-board) and
mount it behind admin auth — it's the standard tool and we've used it before.
## Monitoring and failure modes

## Failure modes
BullMQ retains up to 1,000 completed jobs for one hour and 1,000 failed jobs
for 24 hours; see `DEFAULT_WEBHOOK_JOB_OPTS` in
`src/lib/webhook-queue.ts`. Vercel project Logs show daemon lifecycle and job
errors. The Redis queue should be monitored separately for waiting and failed
jobs.

| Symptom | Diagnosis |
| Symptom | Likely cause |
| --- | --- |
| Webhooks return 500 from `/api/webhooks/github` | `REDIS_URL` missing or Redis unreachable. The request path treats an unavailable queue as a hard failure so GitHub retries. |
| Jobs sit in `waiting` indefinitely | Worker process is down. Restart `worker:webhook`. |
| Same delivery ID retried forever | A handler is throwing on a permanently malformed payload. Check failed jobs; if it's hopeless, manually remove the job via `bull-board` or `Queue.removeJobs()`. |
| `installation.created` events succeed but no DB rows | Worker has a stale `DATABASE_URL` — re-sync env. |
| GitHub webhook returns 500 | Redis is unavailable to the receiver; restore Redis and redeliver the failed event from GitHub if it is not retried automatically. |
| Jobs remain in `waiting` | Daemon has zero instances, has stopped, or cannot connect to Redis. |
| Private repository jobs fail | GitHub App PEM, App ID, or installation access is missing. |
| BYOK jobs fail to decrypt a stored key | `API_KEY_ENCRYPTION_SECRET` is missing or differs from the key used to encrypt it. |

See [Vercel Daemons](https://vercel.com/docs/daemons), its
[configuration reference](https://vercel.com/docs/daemons/config-reference),
and [limits](https://vercel.com/docs/daemons/limits) for current beta behavior.
Loading
Loading