Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 12 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: "1.4.0"

- name: Setup Node for Cloudflare CLI
uses: actions/setup-node@v4
with:
node-version: "22.22.2"

- name: Install dependencies
run: bun install
run: bun install --frozen-lockfile

- name: Check formatting
run: bun run fmt:check
Expand All @@ -33,3 +38,8 @@ jobs:

- name: Test
run: bun run test

- name: Validate Cloudflare build
run: bun run check:cloudflare
env:
CF_SEND_TELEMETRY: "false"
52 changes: 52 additions & 0 deletions .github/workflows/deploy-cloudflare.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Deploy Cloudflare

on:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: pulldash-internal-production
cancel-in-progress: false

jobs:
deploy:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
env:
CF_SEND_TELEMETRY: "false"
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.4.0"

- name: Setup Node for Cloudflare CLI
uses: actions/setup-node@v4
with:
node-version: "22.22.2"

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Check formatting
run: bun run fmt:check

- name: Typecheck
run: bun typecheck

- name: Test
run: bun test

- name: Build and validate deployment
run: bun run check:cloudflare

- name: Deploy validated build
run: bun run cf deploy --prebuilt --mode production
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,8 @@ dist/
release/
public/
.env*.local

# Cloudflare build output and local credentials
.cloudflare/
.wrangler/
.dev.vars*
11 changes: 11 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Local agent state is not project source.
.codex/
.entire/
.pi/

# Generated deployment and application files.
.cloudflare/
.wrangler/
dist/
public/
release/
135 changes: 135 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,141 @@ bun install
bun dev
```

## Internal Cloudflare deployment

The internal site runs at **code.internal.fiet.finance** on Worker
`pulldash-internal`, behind the team's existing Cloudflare Access policy.
PostHog is disabled. GitHub requests still go directly from the browser to GitHub;
only device authentication uses the Worker API.

Use Bun 1.4.0 for installs, builds, and tests. Cloudflare's `cf` CLI must run under
Node 22.22.2; do not force it to use the Bun interpreter. The pinned Wrangler
dependency is only the build backend for `cf`, with no Vite migration.

```bash
bun install --frozen-lockfile
bun run check:cloudflare
```

This builds the browser and API, then validates the production deployment without
uploading it or requiring Cloudflare credentials. Build output lives in
`.cloudflare/`. `cloudflare.config.ts` defines the Worker and runtime settings;
`wrangler.config.ts` selects `dist/browser` as the asset source.

### GitHub login

The team's public client ID, `Iv23li2VXQV3ynTftTS5`, lives in
`src/api/github-config.json`. Set `GITHUB_CLIENT_ID` before building to override
it; local and desktop servers also accept that environment variable.
Device flow must be enabled in the app's GitHub settings. No client secret,
private key, or shared GitHub token belongs in the Worker.

For a GitHub App, install it on the repositories the team reviews and grant the
required repository permissions: contents, pull requests, issues, and actions
read/write; checks and commit statuses read. User tokens are limited by both the app's and the user's
permissions; OAuth scopes do not replace GitHub App permissions. See
[GitHub's user access token documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app).
The existing personal access token login remains available.
Expiring device-flow tokens are stored with their expiry; once expired, users
sign in again. This deployment does not add automatic token refresh.

### First deployment

1. Authenticate `cf` using a scoped `CLOUDFLARE_API_TOKEN`, or run
`bun run cf auth login --no-browser` and complete sign-in from your own browser.
Set `CLOUDFLARE_ACCOUNT_ID` to the account owning the active `fiet.finance` zone.
Keep tokens in an ignored environment file or your secret manager.
2. Check the account, zone, existing Access policies, identity provider, and any
existing DNS record for `code.internal.fiet.finance`. Reuse the team's current
policy; do not replace it with an unrestricted allow policy.
3. Run `bun run deploy:cloudflare:bootstrap`. Bootstrap creates the Worker with no
custom domain, `workers.dev` route, or preview URL. Omitting `--mode` also
defaults to this unpublished configuration.
4. Create or reuse a self-hosted Access application protecting this Worker for
**all traffic**, and attach the existing reusable team policy. Preserve the
existing identity provider and session settings. Use the Worker ID returned by
Cloudflare, not its name, for the Access destination. Use `cf cli search` and
`cf schema` to verify the resource commands before changing account settings.
5. Once Access is attached, run `bun run deploy:cloudflare`. Production mode
attaches `code.internal.fiet.finance`; Cloudflare provisions its HTTPS
certificate. Check authorized and unauthorized access before sharing the URL.

The initial bootstrap deployment is in the **Usher Labs** account
(`a8794e19e02cedc4d87baf4008265dd7`). Its initial version is
`5dbf604a-09f4-483d-b7b7-cc6c0253e1fd`, and its Worker ID is
`88d47ee0269d4952a17d1639cf641f12`. Its reserved
`pulldash-internal.usherlabs.workers.dev` hostname and previews are disabled, and
it has no attached custom domains. The supplied
staging token can deploy Workers, but currently exposes no DNS zones, and that
account reports that Access is not enabled.

To wire DNS later, ensure `fiet.finance` is an active zone in this same account
and use a credential with the required zone/domain permissions. Enable Zero
Trust and attach the team Access policy to `pulldash-internal` before publishing.
Then add `code.internal.fiet.finance` under the Worker's **Domains & Routes → Add
Custom Domain**, or run the production deployment command. Cloudflare creates
the DNS record and certificate automatically; no server IP or manual CNAME to
`workers.dev` is needed. If the zone is owned by a different account, deploy the
Worker in that account before attaching the domain. See
[Cloudflare's Custom Domain requirements](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/).

For the provided dotenvx credential source, inject the token only into the child
process; do not copy it into this repository:

```bash
dotenvx run --redact --strict -f "$HOME/.env" -- bash -c '
export CLOUDFLARE_API_TOKEN="$USHER_LABS_CLOUDFLARE_STAGING_API_TOKEN"
export CLOUDFLARE_ACCOUNT_ID="a8794e19e02cedc4d87baf4008265dd7"
bun run cf auth whoami
'
```

Cloudflare Access admits teammates to the site. Each teammate then authorizes
their own GitHub access. Access protection covers static files and the login API;
the alternative public Worker URLs stay disabled.

### Manual releases and rollback

Add `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` as secrets on
`usherlabs/pulldash`. Scope the token to the deployment account and the Worker and
domain permissions needed to publish it. Access setup is a separate first-release
step; routine deployments must preserve that application and its policies.

After the changes reach `main`, trigger the manual workflow:

```bash
gh workflow run deploy-cloudflare.yml --repo usherlabs/pulldash --ref main
gh run list --repo usherlabs/pulldash --workflow deploy-cloudflare.yml --limit 1
gh run watch <run-id> --repo usherlabs/pulldash --exit-status
```

The workflow runs formatting checks, type checks, tests, a browser/API build, and a
packaging dry run before deploying that exact prebuilt output. Only the publishing
step receives Cloudflare credentials. Pushes and PRs validate packaging but never
publish. Production releases are serialized and restricted to `main`.

After publishing, verify a direct PR URL, `/lib/diff-worker.js`, device login, PAT
login, private PR loading, and rejection of an unauthorized user. No browser
automation is used for this project; browser authentication and review checks
are human smoke tests.

For rollback, use `cf workers deployments list` to identify the previous healthy
Worker version and `cf workers deployments create` to deploy it at 100 percent.
Inspect the installed command help/schema and dry-run the request before applying
it. Rollback changes Worker code and assets; it must leave the domain and Access
policy intact. Keep the previous version ID in the release notes and inspect
Cloudflare Worker/Access logs after a release.

```bash
bun run cf workers deployments list --worker pulldash-internal
bun run cf workers deployments create --worker pulldash-internal \
--strategy percentage \
--versions '[{"version_id":"<previous-version-id>","percentage":100}]' \
--dry-run
```

After reviewing the dry run, repeat the create command without `--dry-run`.

## License

[AGPL](./LICENSE)
73 changes: 73 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Security and privacy review

Reviewed on 2026-10-14 for the internal Cloudflare deployment at
`code.internal.fiet.finance`. This document records the current protections and
open hardening work; it is not a penetration test or a guarantee of security.

## Telemetry and data recipients

- PostHog was removed from the application and dependencies. Telemetry calls are
no-ops in `src/browser/contexts/telemetry.tsx`. No other analytics SDK or
intentional reporting to the upstream Pulldash operators was found in the
reviewed source or locally built frontend assets.
- GitHub API requests for PRs, reviews, and file contents go directly from the
browser to GitHub. Diff parsing and highlighting happen locally in browser
workers. The application has no server-side repository database.
- Device authorization is proxied through the team's Cloudflare Worker to
GitHub. The access token returned by GitHub passes through that Worker to the
browser. Authentication responses use `Cache-Control: no-store`; the
application does not persist tokens server-side.
- Cloudflare Worker logs and traces remain enabled in the team's account.
Request URLs, status codes, timings, and related metadata may be recorded.
Repository identifiers can appear in application URLs. No external
observability export, Logpush, or tail consumer was configured for this Worker
at review time. See [Workers Logs](https://developers.cloudflare.com/workers/observability/logs/workers-logs/)
and [Workers traces](https://developers.cloudflare.com/workers/observability/traces/).
- PR descriptions and comments can contain external images. Rendering an image
can send the viewer's IP address and request metadata to its host. The
application does not intentionally attach GitHub authorization headers to
those image requests. Some GitHub images are proxied, but the application does
not enforce a GitHub-only image policy.

## Confirmed deployment protections

- Cloudflare Access protects the Worker, including the site and authentication
API. Anonymous HTTP probes redirect to the Access login page.
- The configured allow policy requires an email address under `usher.so`.
The Access session duration is seven days.
- `workers.dev` access and preview URLs are disabled.
- GitHub and the application use HTTPS. Repository permissions are still
enforced by GitHub independently of the Cloudflare Access policy.
- The configured GitHub client ID is public configuration, not a client secret.

## Open hardening work

1. **Protect GitHub tokens in the browser.** Tokens are stored in `localStorage`
in `src/browser/contexts/auth.tsx`. JavaScript running on the application's
origin can read them; an XSS vulnerability could expose them. Tokens can
remain stored after a Cloudflare Access session ends. Consider memory-only
storage, or a server-side session with an HttpOnly cookie if stronger token
isolation is required. See [OWASP storage guidance](https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html#storage-apis).
2. **Clear or isolate private local state.** Normal logout clears the token and
GitHub request cache. Draft comments, review text, viewed filenames, and tab
state persist separately in `localStorage`. Their keys are not scoped to the
GitHub account. Clear sensitive state on logout or isolate it by user, and
handle token replacement/account switching explicitly.
3. **Restrict rendered content and outbound resources.** Add a Content Security
Policy and an explicit referrer policy, restrict external images or require
user consent before loading them, and sanitize GitHub-rendered HTML locally.
The Markdown path uses `rehype-sanitize`; the pre-rendered HTML path currently
relies on GitHub's sanitization instead of applying a local sanitizer.
4. **Review permissions and logging.** Verify the team's GitHub App ownership,
device-flow setting, permissions, and installations. Prefer the minimum
repository access needed. Decide whether full Worker log/trace sampling is
necessary and whether URL query strings should be redacted.

## Review scope and limitations

The review covered application source, locally built frontend assets, the active
Worker's API module, Cloudflare deployment settings, Access policy, and anonymous
HTTP probes. It did not inspect an authenticated browser's live network traffic,
download the deployed frontend assets, or verify the GitHub App's permissions in
its management settings. Findings should be revisited after deployment,
authentication, rendering, or dependency changes.
Loading
Loading