Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
30d7dd3
docs(deploy): add WAF rules, Workers Secrets, and local dev documenta…
wgordon17 Apr 9, 2026
c7745fc
feat(worker): add crypto module for sealed tokens and session signing
wgordon17 Apr 9, 2026
94e16dd
feat(worker): adds session cookie infrastructure
wgordon17 Apr 9, 2026
af55386
feat(worker): integrate security middleware into fetch handler
wgordon17 Apr 9, 2026
94de310
feat(auth): adds Turnstile widget, seal helper, proxy utilities
wgordon17 Apr 9, 2026
908a195
fix(worker): address security and QA review findings
wgordon17 Apr 9, 2026
8fedd8d
docs: add /api/proxy/seal endpoint to DEPLOY.md API table
wgordon17 Apr 9, 2026
55b70c7
fix(worker): pass sessionId to handleProxySeal for SC-11 audit logging
wgordon17 Apr 9, 2026
39215e4
fix(worker): add rate limiter error handling and invalid purpose test
wgordon17 Apr 9, 2026
a28a80d
fix(security): adds Turnstile domains to CSP, removes dead body.code …
wgordon17 Apr 9, 2026
7a40118
refactor(worker): removes dead code, uses SealError class
wgordon17 Apr 9, 2026
a0fc6dc
fix(worker): addresses PR review findings — security, perf, tests, docs
wgordon17 Apr 9, 2026
5e6f444
fix(turnstile): harden widget lifecycle
wgordon17 Apr 12, 2026
6a9773a
fix(turnstile): hardens widget lifecycle edge cases
wgordon17 Apr 12, 2026
721c63d
fix(turnstile): addresses adversarial review findings
wgordon17 Apr 12, 2026
28104b1
fix(turnstile): moves setTimeout above ready() call
wgordon17 Apr 12, 2026
e1c9930
feat(worker): adds KAT, fuzz tests, timing-safe verify
wgordon17 Apr 12, 2026
9a23756
feat(worker): adds fail-fast hardening to tunnel and proxy endpoints
wgordon17 Apr 13, 2026
7564caa
fix(worker): uses shared test helpers and fixes comment
wgordon17 Apr 13, 2026
fa63140
fix(worker): moves prune before rate limit check, uses shared helpers
wgordon17 Apr 13, 2026
e5813d5
fix(worker): adds Origin null tests, standardizes consoleSpy naming
wgordon17 Apr 13, 2026
2bc0957
feat(worker): hardens IP, key cache, CSP scrub, token rate limit
wgordon17 Apr 13, 2026
9a27ab3
fix(worker): rejects missing IP, validates rate limiter binding exists
wgordon17 Apr 13, 2026
e1fbcff
test(worker): adds missing-IP and missing-binding tests, updates docs
wgordon17 Apr 13, 2026
783ad36
docs(privacy): disclose session cookie, Turnstile, logging
wgordon17 Apr 13, 2026
71aacbe
fix(sentry): wraps ErrorBoundary with Sentry capture for render errors
wgordon17 Apr 13, 2026
4daf145
fix(sentry): adds captureException to catch blocks, expands breadcrumbs
wgordon17 Apr 13, 2026
50909bd
feat(worker): adds @sentry/cloudflare for worker-side error capture
wgordon17 Apr 13, 2026
0f2a752
fix(sentry): address review findings — browser scrubbing parity, cleanup
wgordon17 Apr 13, 2026
5b40823
test(sentry): adds browser scrubbing and client_secret URL tests
wgordon17 Apr 13, 2026
dc85611
refactor(sentry): simplifies post-review
wgordon17 Apr 13, 2026
6d08375
fix(sentry): adds github_pat_ to worker scrubbing regex
wgordon17 Apr 13, 2026
61fc680
fix(sentry): harmonizes scrubbing regex and redaction labels
wgordon17 Apr 13, 2026
b1c9050
fix(sentry): selective integrations, case-insensitive scrub
wgordon17 Apr 13, 2026
0e9f43b
refactor(worker): consolidates dual @sentry/cloudflare imports
wgordon17 Apr 13, 2026
1a758c5
fix(sentry): adds DSN cross-reference and ExecutionContext comments
wgordon17 Apr 13, 2026
0e0d4aa
fix(worker): extends CSP scrubbing with token patterns
wgordon17 Apr 13, 2026
3713159
fix(worker): requires Origin on CSP report endpoint
wgordon17 Apr 13, 2026
a17d618
fix(sentry): externalize DSN to VITE_SENTRY_DSN env var
wgordon17 Apr 13, 2026
1be3e1e
fix(mcp): removes hardcoded domain from relay allowed origins
wgordon17 Apr 13, 2026
1856c29
fix(deploy): makes WAF test domain configurable, adds deploy comments
wgordon17 Apr 13, 2026
4602c47
feat(deploy): adds deploy validation script
wgordon17 Apr 13, 2026
8515913
docs(deploy): adds fork deployment checklist
wgordon17 Apr 13, 2026
1ccaede
fix: address review findings from Phase 4
wgordon17 Apr 13, 2026
da3bbc9
test(sentry): stub DEV=false in no-op DSN tests
wgordon17 Apr 13, 2026
712c647
fix: address 14 PR review findings
wgordon17 Apr 14, 2026
dbacd73
refactor(worker): reuse crypto.ts base64url helpers in session.ts
wgordon17 Apr 14, 2026
7d788f8
docs(deploy): note WAF exemptions are simplifiable post-deploy
wgordon17 Apr 14, 2026
4953c38
docs(deploy): removes one-time WAF note
wgordon17 Apr 14, 2026
388f442
fix: address PR review findings — security docs, tests, code quality
wgordon17 Apr 14, 2026
44efe87
fix(mcp): adds production origin to defaults
wgordon17 Apr 14, 2026
3c81d3b
fix(deploy): adds sync guard for validate-deploy.sh
wgordon17 Apr 15, 2026
88043c1
docs(deploy): restructure checklist into OAuth and static-only paths
wgordon17 Apr 15, 2026
875d131
docs(deploy): removes GitLab references
wgordon17 Apr 15, 2026
4abde8e
fix(deploy): unify validate-deploy, rename key rotation to _NEXT
wgordon17 Apr 15, 2026
df0a5b5
docs(deploy): separate session and seal key rotation lifecycles
wgordon17 Apr 15, 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
5 changes: 5 additions & 0 deletions .dev.vars.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
GITHUB_CLIENT_ID=your_client_id_here
GITHUB_CLIENT_SECRET=your_client_secret_here
ALLOWED_ORIGIN=http://localhost:5173
SESSION_KEY=your-base64-encoded-32-byte-key
SEAL_KEY=your-base64-encoded-32-byte-key
TURNSTILE_SECRET_KEY=your-turnstile-secret-from-cf-dashboard
# Optional: only needed if Sentry "Allowed Domains" is configured in your Sentry project settings
# SENTRY_SECURITY_TOKEN=your-sentry-security-token
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,15 @@ GITHUB_TOKEN=your_github_token_here
# Port for the WebSocket relay server (MCP ↔ browser dashboard bridge).
# Default: 9876
# MCP_WS_PORT=9876

# ── Turnstile (Cloudflare) ─────────────────────────────────────────────────────
# Public site key — embedded into client-side bundle at build time by Vite.
# This is public information (visible in the Turnstile widget script).
# Get this from the Cloudflare Turnstile dashboard.
# Note: TURNSTILE_SECRET_KEY is a Worker secret (goes in .dev.vars, not .env).
VITE_TURNSTILE_SITE_KEY=your-turnstile-site-key-from-cf-dashboard

# ── Sentry (optional) ─────────────────────────────────────────────────────────
# Sentry DSN for error reporting. Leave empty to disable Sentry.
# Get this from your Sentry project's Client Keys (DSN) settings.
# VITE_SENTRY_DSN=https://your-public-key@o12345.ingest.us.sentry.io/your-project-id
13 changes: 12 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,21 @@ jobs:
- name: Verify CSP inline script hash
run: bash scripts/verify-csp-hash.sh
- name: WAF smoke tests
run: pnpm test:waf
if: vars.DEPLOY_DOMAIN != ''
run: pnpm test:waf "https://${{ vars.DEPLOY_DOMAIN }}"
- name: Validate deploy configuration
run: pnpm validate:deploy
env:
VITE_GITHUB_CLIENT_ID: ${{ vars.VITE_GITHUB_CLIENT_ID }}
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
VITE_TURNSTILE_SITE_KEY: ${{ vars.VITE_TURNSTILE_SITE_KEY }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- run: pnpm run build
env:
VITE_GITHUB_CLIENT_ID: ${{ vars.VITE_GITHUB_CLIENT_ID }}
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
VITE_TURNSTILE_SITE_KEY: ${{ vars.VITE_TURNSTILE_SITE_KEY }}
- uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
Expand Down
214 changes: 210 additions & 4 deletions DEPLOY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,87 @@
# Deployment Guide

## Fork Deployment Checklist

If you're deploying your own instance of GitHub Tracker, choose a deployment path:

- **OAuth + Cloudflare Worker** — "Sign in with GitHub" button, requires a backend for the OAuth client secret exchange
- **Static-only (PAT)** — Host anywhere, no backend needed, users paste a Personal Access Token

### Static-only deployment (no Cloudflare Worker)

GitHub Tracker can run as a pure static site without the Cloudflare Worker backend.
Use a Personal Access Token (PAT) instead of OAuth — the PAT flow validates directly
against `api.github.com` with no server-side component.

**Host the `dist/` build output on any static platform:** GitHub Pages, Netlify, Vercel,
S3 + CloudFront, or any CDN that serves SPAs with `index.html` fallback for client-side
routing.

**What works without a backend:**
- All dashboard features (Issues, PRs, Actions tabs)
- PAT authentication (classic `ghp_` or fine-grained `github_pat_`)
- All GitHub API calls (GraphQL + REST, direct to `api.github.com`)
- Full poll + hot poll refresh cycles
- Desktop notifications
- Multi-user tracking, upstream repo discovery, monitor-all mode
- Repo pinning/reordering, themes, ignore system
- IndexedDB caching + ETag optimization
- MCP server (separate Node.js process, independent of Worker)

**What does NOT work without a backend:**
- **OAuth login** — requires server-side `client_secret` exchange. Use a PAT instead.
The "Sign in with GitHub" button will still appear on the login page but will fail
if no `VITE_GITHUB_CLIENT_ID` is configured.
- **Sentry error reporting** — do NOT set `VITE_SENTRY_DSN` on static-only deploys.
The Sentry SDK is configured with `tunnel: "/api/error-reporting"`, which doesn't
exist on a static host. Setting a DSN causes the SDK to silently lose all error
reports via 404s. Leave `VITE_SENTRY_DSN` empty to cleanly disable Sentry.
- **CSP violation reporting** — reports silently dropped (no user impact). The
`report-uri /api/csp-report` directive in `public/_headers` will produce harmless
404 console errors on static hosts. Optionally remove the `report-uri` and
`report-to` directives if the noise is unwanted.
- **Jira token sealing** (planned) — requires server-side encryption

**Security note:** The `public/_headers` file sets Content-Security-Policy and other
security headers. Ensure your static host serves these headers — Cloudflare Pages,
Netlify, and Vercel support `_headers` files natively. Other hosts may need manual
header configuration.

**Build for static deployment:**
```sh
pnpm install
pnpm run build # Output in dist/
# Upload dist/ to your static host
```
No `VITE_GITHUB_CLIENT_ID` or `VITE_TURNSTILE_SITE_KEY` is needed for PAT-only
deployments — leave them empty. OAuth login won't work without a client ID (use
PAT instead), and Turnstile is only used by the planned Jira integration.

### OAuth + Cloudflare Worker

1. **Create a GitHub OAuth App** — [Settings → Developer settings → OAuth Apps](https://github.com/settings/developers)
- Set the callback URL to `https://YOUR-DOMAIN/oauth/callback`
2. **Update `wrangler.toml`** — Change `pattern = "gh.gordoncode.dev"` to your domain
3. **Set GitHub Actions secrets and variables** — See sections below
4. **Set Cloudflare Worker secrets** — See "Cloudflare Worker Secrets" section below. **Critical:** `ALLOWED_ORIGIN` must exactly match your deployment URL (e.g., `https://your-domain.example.com`). An incorrect value causes all API requests to fail with CORS errors.

**Verify configuration:** Run `pnpm validate:deploy` locally to check that all required
Cloudflare Worker secrets are set. In CI, the deploy workflow runs
`pnpm validate:deploy --ci` automatically before building.

### Optional (both deployment paths)

5. **MCP relay** — If deploying to a custom domain, set `MCP_RELAY_ALLOWED_ORIGINS=https://YOUR-DOMAIN` when running the MCP server (`https://gh.gordoncode.dev` is allowed by default)
6. **Sentry error reporting** — Set `VITE_SENTRY_DSN` (build-time, via GitHub Actions variable) and `SENTRY_DSN` (Worker secret, via `wrangler secret put`) to the **same** DSN value. The Worker tunnel (`/api/error-reporting`) validates the incoming envelope DSN against `env.SENTRY_DSN` — different values cause all Sentry events to silently return 403. Leave both empty to disable. **Worker-only** — does not work on static deploys.
7. **Cloudflare Turnstile** — Create a widget and set `VITE_TURNSTILE_SITE_KEY` in `.env`. Only needed for the planned Jira token sealing feature. **Worker-only.**
8. **WAF smoke tests** — Set `DEPLOY_DOMAIN` as a GitHub Actions variable (e.g., `your-domain.example.com`). CI runs `pnpm test:waf https://$DEPLOY_DOMAIN` automatically. If `DEPLOY_DOMAIN` is not set, the WAF test is skipped. Run locally with: `pnpm test:waf https://YOUR-DOMAIN`. **Worker-only.**
9. **Social metadata** — Update `og:image` and `og:url` in `index.html` to your domain
10. **Security contact** — Update the email and scope domain in `SECURITY.md`
11. **README** — Update the "Live demo" URL in `README.md`
12. **User Guide** — Update the domain reference in `docs/USER_GUIDE.md`
13. **App footer links** — Update "Source" and "Guide" URLs in `src/app/components/dashboard/DashboardPage.tsx` and `src/app/components/settings/SettingsPage.tsx` if you want them to point to your fork
14. **Contributing guide** — Update the clone URL and PR target in `CONTRIBUTING.md`

## GitHub Actions Secrets and Variables

### Secrets (GitHub repo → Settings → Secrets and variables → Actions → Secrets)
Expand Down Expand Up @@ -29,8 +111,8 @@
1. Go to GitHub → Settings → Developer settings → OAuth Apps → **New OAuth App**
2. Fill in the details:
- **Application name**: your app name (e.g. `gh-tracker-yourname`)
- **Homepage URL**: `https://gh.gordoncode.dev`
- **Authorization callback URL**: `https://gh.gordoncode.dev/oauth/callback`
- **Homepage URL**: `https://YOUR-DOMAIN` (e.g. `https://gh.gordoncode.dev`)
- **Authorization callback URL**: `https://YOUR-DOMAIN/oauth/callback`
3. Click **Register application**
4. Note the **Client ID** — this is your `VITE_GITHUB_CLIENT_ID`
5. Click **Generate a new client secret** and save it for the Worker secrets below
Expand Down Expand Up @@ -67,14 +149,15 @@ wrangler secret put ALLOWED_ORIGIN

- `GITHUB_CLIENT_ID`: same value as `VITE_GITHUB_CLIENT_ID`
- `GITHUB_CLIENT_SECRET`: the Client Secret from your GitHub OAuth App
- `ALLOWED_ORIGIN`: `https://gh.gordoncode.dev`
- `ALLOWED_ORIGIN`: `https://YOUR-DOMAIN` (e.g. `https://gh.gordoncode.dev`)

## Worker API Endpoints

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/oauth/token` | POST | Exchange OAuth authorization code for permanent access token. |
| `/api/health` | GET | Health check. Returns `OK`. |
| `/api/proxy/seal` | POST | Encrypt an API token for client-side storage. Requires Turnstile + session. |

### Token Storage Security

Expand All @@ -89,12 +172,32 @@ The OAuth App access token is a permanent credential (no expiry). It is stored i
### CORS

- `Access-Control-Allow-Origin`: exact match against `ALLOWED_ORIGIN` (no wildcards)
- No `Access-Control-Allow-Credentials` header (OAuth App uses no cookies)
- No `Access-Control-Allow-Credentials` header (the `__Host-session` cookie is SameSite=Strict and is not relevant to cross-origin requests)

### Tunnel Endpoint Security

The two tunnel endpoints (`/api/error-reporting` and `/api/csp-report`) receive untrusted browser data and forward it to Sentry. They are hardened with layered fail-fast guards (IP rate limit → Origin check → DSN validation). See `hack/docs/security-runbook.md` for the full threat model, per-endpoint rate limit values, Origin check behavior, CSP field sanitization details, Content-Length pre-check semantics, and fan-out amplification analysis.

## Local Development

Copy `.dev.vars.example` to `.dev.vars` and fill in your values. Wrangler picks up `.dev.vars` automatically for local `wrangler dev` runs.

### HTTPS requirement for session cookies

The `__Host-session` cookie uses the `__Host-` prefix, which browsers **silently reject over HTTP**. To test session cookies locally, use:

```bash
wrangler dev --local-protocol https
```

The self-signed certificate from `--local-protocol https` must be accepted in the browser on first use (click through the "Not Secure" warning or add a security exception).

### Compatibility flags in local dev

The `global_fetch_strictly_public` compatibility flag (which blocks Worker subrequests to private/internal IPs) has **no effect** in local `wrangler dev` — workerd ignores it. No local dev workaround is needed for this flag.

The `nodejs_als` compatibility flag is required by `@sentry/cloudflare` for `AsyncLocalStorage` (request context propagation). It is declared in `wrangler.toml` and is active in both production and local dev.

## Deploy Manually

```sh
Expand All @@ -114,3 +217,106 @@ If you previously deployed with the GitHub App model (HttpOnly cookie refresh to
6. **Delete the old GitHub App** (optional): GitHub → Settings → Developer settings → GitHub Apps → your app → Advanced → Delete

The old `POST /api/oauth/refresh` and `POST /api/oauth/logout` endpoints no longer exist and return 404.

---

## WAF Security Rules

Configure these rules in the Cloudflare dashboard under **Security → WAF**.

### Custom Rules

**Rule name:** Block API requests without valid Origin
**Where:** Security → WAF → Custom Rules
**Expression:**
```
(http.request.uri.path starts_with "/api/") and
not (any(http.request.headers["origin"][*] in {"https://YOUR-DOMAIN"})) and
not (http.request.uri.path eq "/api/csp-report") and
not (http.request.uri.path eq "/api/error-reporting")
```
**Action:** Block

**Exemptions:** `/api/csp-report` and `/api/error-reporting` are excluded because the Worker enforces its own strict origin check on both endpoints. See `hack/docs/security-runbook.md` for exemption rationale.

**Notes:**
- This uses **1 of the 5 free WAF custom rules** available on all plans.
- Blocks scanners, `curl` without `Origin`, and cross-site browser attacks before the Worker runs (never billed as a Worker request).

### Rate Limiting Rules

> **Conditional:** WAF rate limiting rules may require a **Pro plan** or above. If unavailable on your current Cloudflare plan (Free plan), skip this step. The Workers Rate Limiting Binding provides per-session rate limiting instead, and the WAF custom rule (above) still enforces the Origin check layer.

**Rule name:** Rate limit API proxy endpoints
**Where:** Security → WAF → Rate Limiting Rules
**Matching expression:**
```
(http.request.uri.path starts_with "/api/") and
(http.request.method ne "OPTIONS")
```
**Rate:** 60 requests per 10 seconds per IP
**Action:** Block for 60 seconds

See `hack/docs/security-runbook.md` for implementation details.

---

## Workers Secrets

All secrets are set via the `wrangler` CLI and stored in the Cloudflare Worker runtime (never committed to source control).

### Generating keys

```bash
# Generate cryptographically strong keys (base64-encoded 32-byte random values):
openssl rand -base64 32 # Run once per key below
```

### Setting secrets

```bash
wrangler secret put SESSION_KEY # HKDF input key material for session cookies
wrangler secret put SEAL_KEY # HKDF input key material for sealed tokens
wrangler secret put TURNSTILE_SECRET_KEY # From Cloudflare Turnstile dashboard
```

- `SESSION_KEY`: HKDF input key material used to derive the HMAC-SHA256 key for signing `__Host-session` cookies. Generate with `openssl rand -base64 32`.
- `SENTRY_DSN` (Worker secret, set via `wrangler secret put SENTRY_DSN`): used by both the Sentry tunnel endpoint for DSN validation and the `@sentry/cloudflare` SDK for direct worker-side error capture. Sentry DSNs are public keys — they authorize sending events, not reading them. Must match the `VITE_SENTRY_DSN` build-time env var; a mismatch causes tunnel requests to return 403.
- `SENTRY_SECURITY_TOKEN` (**optional**, set via `wrangler secret put SENTRY_SECURITY_TOKEN`): only needed if you have configured "Allowed Domains" in your Sentry project's security settings. The Worker sends this token as the `X-Sentry-Token` HTTP header on outbound requests to Sentry's envelope and CSP report endpoints. Leave unset if Allowed Domains is not configured.
- `SEAL_KEY`: HKDF input key material used to derive the AES-256-GCM key for encrypting API tokens stored client-side as sealed blobs. Generate with `openssl rand -base64 32`.
- `TURNSTILE_SECRET_KEY`: From the Cloudflare Turnstile dashboard (Security → Turnstile → your widget → Secret key).
- `VITE_TURNSTILE_SITE_KEY`: **Build-time env var (public)** — goes in `.env`, not a Worker secret. From the same Turnstile dashboard (Site key).

### First deployment

On initial deployment, set only `SESSION_KEY`, `SEAL_KEY`, and `TURNSTILE_SECRET_KEY`. Do **not** set `SESSION_KEY_NEXT` or `SEAL_KEY_NEXT` — these are only needed during key rotation.

### Key rotation

Session keys and seal keys have different rotation lifecycles because their tokens have different lifetimes.

**Session key** (`SESSION_KEY`) — sessions expire after 8 hours (`__Host-session` cookie `Max-Age`), so the transition window is short:

1. Generate a new key and set it as `SESSION_KEY_NEXT`:
```bash
openssl rand -base64 32 # save this value
wrangler secret put SESSION_KEY_NEXT
```
2. The Worker signs new sessions with `SESSION_KEY_NEXT` and verifies with both keys.
3. After 8 hours (all old sessions expired), promote and clean up:
```bash
wrangler secret put SESSION_KEY # paste the same value from step 1
wrangler secret delete SESSION_KEY_NEXT
```

**Seal key** (`SEAL_KEY`) — sealed tokens (e.g., Jira API tokens) are stored in the client's `localStorage` with no expiry. They persist until the user re-seals or clears storage:

1. Generate a new key and set it as `SEAL_KEY_NEXT`:
```bash
openssl rand -base64 32 # save this value
wrangler secret put SEAL_KEY_NEXT
```
2. The Worker seals new tokens with `SEAL_KEY_NEXT` and unseals with both keys.
3. **Do not promote until all clients have re-sealed their tokens.** Promoting `SEAL_KEY` and deleting `SEAL_KEY_NEXT` makes tokens sealed with the old key permanently unreadable. If you cannot ensure all clients have re-sealed, keep `SEAL_KEY_NEXT` set indefinitely — the Worker handles both keys with no performance penalty.

**Why `_NEXT` instead of `_PREV`?** Cloudflare Worker secrets are write-only — you cannot read back a secret's value. A `_PREV` design requires knowing the current key value to copy it, which is impossible to retrieve. With `_NEXT`, you only need the value you just generated.
2 changes: 1 addition & 1 deletion e2e/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ test("sign out clears auth and redirects to login", async ({ page }) => {
);
expect(authToken).toBeNull();

// Verify config was reset (SDR-016 data leakage prevention).
// Verify config was reset (data leakage prevention).
// The persistence effect may re-write defaults, so check that user-specific
// data (selectedOrgs, onboardingComplete) was cleared rather than checking null.
const configEntry = await page.evaluate(() =>
Expand Down
1 change: 1 addition & 0 deletions mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ npm install -g github-tracker-mcp
|----------|----------|---------|-------------|
| `GITHUB_TOKEN` | No | — | Classic PAT with `repo` and `read:org` scopes (recommended), or fine-grained PAT with Actions (read), Contents (read), Issues (read), and Pull requests (read) permissions. Fine-grained PATs skip scope validation at startup. |
| `MCP_WS_PORT` | No | `9876` | WebSocket relay port for receiving live data from the dashboard SPA. |
| `MCP_RELAY_ALLOWED_ORIGINS` | No | — | Comma-separated additional origins for WebSocket connections. Only needed if you deploy to a custom domain (the default `https://gh.gordoncode.dev` and localhost origins are always allowed). |

`GITHUB_TOKEN` is required for standalone (direct API) mode. In relay mode the server receives data from the dashboard and works without a token. If you set `GITHUB_TOKEN` alongside the relay, the server uses it as a fallback when the relay disconnects.

Expand Down
5 changes: 5 additions & 0 deletions mcp/src/ws-relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ function buildAllowedOrigins(): Set<string> {
// Computed once at module scope — origins don't change at runtime
const ALLOWED_ORIGINS = buildAllowedOrigins();

// Log extra origins if configured (useful for custom deployments / forks)
if (process.env.MCP_RELAY_ALLOWED_ORIGINS) {
console.error("[mcp/ws] Additional allowed origins:", process.env.MCP_RELAY_ALLOWED_ORIGINS);
}

function isOriginAllowed(origin: string | undefined): boolean {
// Non-browser clients (e.g. CLI tools) do not send Origin — allow them.
if (origin === undefined) return true;
Expand Down
Loading