Skip to content
Closed
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
165 changes: 165 additions & 0 deletions .github/workflows/cloudfree-upstream-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
name: CloudFree Upstream Sync

on:
schedule:
# Check upstream daily at 6am UTC
- cron: "0 6 * * *"
workflow_dispatch:
inputs:
upstream_branch:
description: "Upstream branch to sync from"
default: "main"
type: string

permissions:
contents: write
pull-requests: write

jobs:
sync-and-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout cloudfree fork
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Configure git
run: |
git config user.name "CloudFree Bot"
git config user.email "cloudfree-bot@users.noreply.github.com"

- name: Add upstream remote
run: |
git remote add upstream https://github.com/OpenWhispr/openwhispr.git || true
git fetch upstream

- name: Check for new upstream commits
id: check
run: |
UPSTREAM_BRANCH="${{ inputs.upstream_branch || 'main' }}"
LOCAL_SHA=$(git rev-parse HEAD)
UPSTREAM_SHA=$(git rev-parse "upstream/${UPSTREAM_BRANCH}")

if [ "$LOCAL_SHA" = "$UPSTREAM_SHA" ]; then
echo "No new upstream commits"
echo "has_changes=false" >> $GITHUB_OUTPUT
else
COMMIT_COUNT=$(git rev-list HEAD.."upstream/${UPSTREAM_BRANCH}" --count)
echo "Found ${COMMIT_COUNT} new upstream commits"
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "commit_count=${COMMIT_COUNT}" >> $GITHUB_OUTPUT
echo "upstream_branch=${UPSTREAM_BRANCH}" >> $GITHUB_OUTPUT
fi

- name: Create sync branch
if: steps.check.outputs.has_changes == 'true'
id: branch
run: |
BRANCH="cloudfree/upstream-sync-$(date +%Y%m%d)"
git checkout -b "$BRANCH"
echo "branch_name=${BRANCH}" >> $GITHUB_OUTPUT

- name: Merge upstream
if: steps.check.outputs.has_changes == 'true'
id: merge
run: |
UPSTREAM_BRANCH="${{ steps.check.outputs.upstream_branch }}"
if git merge "upstream/${UPSTREAM_BRANCH}" --no-edit; then
echo "merge_status=clean" >> $GITHUB_OUTPUT
else
echo "merge_status=conflict" >> $GITHUB_OUTPUT
git merge --abort
fi

- name: Setup Node.js
if: steps.check.outputs.has_changes == 'true' && steps.merge.outputs.merge_status == 'clean'
uses: actions/setup-node@v4
with:
node-version: 22

- name: Scan for new network calls
if: steps.check.outputs.has_changes == 'true' && steps.merge.outputs.merge_status == 'clean'
id: scan
run: |
# Scan the diff for new network endpoints
node scripts/cloudfree/scan-network-calls.js --diff main 2>&1 | tee scan-report.txt

# Check if there are issues (unknown or blocked domains)
if node scripts/cloudfree/scan-network-calls.js --diff main --ci 2>/dev/null; then
echo "scan_clean=true" >> $GITHUB_OUTPUT
else
echo "scan_clean=false" >> $GITHUB_OUTPUT
fi
continue-on-error: true

- name: Push sync branch
if: steps.check.outputs.has_changes == 'true' && steps.merge.outputs.merge_status == 'clean'
run: |
git push origin "${{ steps.branch.outputs.branch_name }}"

- name: Create PR
if: steps.check.outputs.has_changes == 'true' && steps.merge.outputs.merge_status == 'clean'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
SCAN_STATUS=""
if [ "${{ steps.scan.outputs.scan_clean }}" = "true" ]; then
SCAN_STATUS="✅ No new network endpoints detected"
else
SCAN_STATUS="⚠️ New network endpoints detected — review required"
fi

SCAN_REPORT=$(cat scan-report.txt 2>/dev/null || echo "No scan report available")

gh pr create \
--title "CloudFree: Sync ${{ steps.check.outputs.commit_count }} upstream commits" \
--body "$(cat <<EOF
## Upstream Sync

Merging **${{ steps.check.outputs.commit_count }}** new commits from \`upstream/${{ steps.check.outputs.upstream_branch }}\`.

## Network Scan Result

${SCAN_STATUS}

<details>
<summary>Full scan report</summary>

\`\`\`
${SCAN_REPORT}
\`\`\`

</details>

## Review Checklist

- [ ] No new cloud-only endpoints introduced
- [ ] No new analytics/telemetry code
- [ ] No new auth/billing dependencies
- [ ] cloudfree-allowlist.json updated if new legitimate endpoints added
EOF
)" \
--label "upstream-sync"

- name: Create conflict issue
if: steps.check.outputs.has_changes == 'true' && steps.merge.outputs.merge_status == 'conflict'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh issue create \
--title "CloudFree: Upstream sync has merge conflicts" \
--body "$(cat <<EOF
Automated upstream sync from \`upstream/${{ steps.check.outputs.upstream_branch }}\` has merge conflicts.

**${{ steps.check.outputs.commit_count }}** new upstream commits need to be merged manually.

\`\`\`bash
git fetch upstream
git merge upstream/${{ steps.check.outputs.upstream_branch }}
# resolve conflicts per CLOUDFREE.md
git commit
\`\`\`
EOF
)" \
--label "upstream-sync"
192 changes: 192 additions & 0 deletions CLOUDFREE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# CloudFree OpenWhispr — Fork Maintenance Guide

A privacy-hardened fork of OpenWhispr. All CloudFree-specific code lives in `src/cloudfree/` with minimal surgical edits to upstream files. This document describes every upstream modification so changes can be re-applied after pulling from upstream.

## Architecture

CloudFree has three layers:

1. **`src/cloudfree/hooks.js`** — Main/preload process hooks (network guard, IPC bindings)
2. **`src/cloudfree/ui-hooks.tsx`** — Renderer-side exports (settings map, network panel, shield icon)
3. **`cloudfree-allowlist.json`** — Domain+path allowlist enforced at the Electron session level

All upstream UI components remain intact. CloudFree injects behavior through imports and small patches at file boundaries (top-of-file imports, end-of-array callbacks, export swaps).

## New Files (CloudFree-owned, no upstream equivalent)

| File | Purpose |
|------|---------|
| `cloudfree-allowlist.json` | Network allowlist — domain+path rules with wildcard support |
| `src/cloudfree/hooks.js` | Main/preload process hooks (`onBeforeReady`, `preload`) |
| `src/cloudfree/networkGuard.js` | Electron `session.webRequest.onBeforeRequest` filter + IPC handlers |
| `src/cloudfree/ui-hooks.tsx` | Settings sidebar map, hidden sections set, CloudFree pane item, re-exports |
| `src/cloudfree/branding.ts` | Fork-specific names, URLs, support links, attribution text |
| `src/cloudfree/CloudFreeSupportDropdown.tsx` | Fork support dropdown (bug report link + upstream attribution) |
| `src/cloudfree/components/CloudFreeNetworkPanel.tsx` | Network guard visualization (live log, stats, allowlist viewer) |
| `src/assets/fonts/noto-sans.css` | Local @font-face declarations for bundled Noto Sans |
| `src/assets/fonts/NotoSans-*.woff2` | Bundled font files (4 files, ~430KB total) |
| `scripts/cloudfree/scan-network-calls.js` | Static analysis script to find network URLs in codebase |
| `.github/workflows/cloudfree-upstream-sync.yml` | Daily upstream sync workflow with network scan |

## Upstream File Modifications

Each modification is described so it can be re-applied mechanically after an upstream merge.

### `main.js` (+6 lines)

**At top, after the unhandled rejection handler (~line 167):**
```js
// CloudFree hooks — pluggable integration points for the fork
const cloudfree = require("./src/cloudfree/hooks");
```

**At the start of `startApp()` function, before `initializeCoreManagers()`:**
```js
// CloudFree: Run main process hooks before any other network activity
cloudfree.onBeforeReady({ session: session.defaultSession, debugLogger });
```

### `preload.js` (+4 lines)

**At top, after the electron require:**
```js
const cloudfree = require("./src/cloudfree/hooks");
```

**At the end of the `contextBridge.exposeInMainWorld("electronAPI", {` object, before the closing `});`:**
```js
// CloudFree hooks
...cloudfree.preload(ipcRenderer),
```

### `src/components/SettingsModal.tsx` (+3 lines changed)

**Add import after the SettingsPage import:**
```ts
import * as cloudfree from "../cloudfree/ui-hooks";
```

**Change the closing of the sidebar items array from `],` to:**
```ts
].flatMap(cloudfree.settingsMap),
```

**Change the default active section from `"account"` to:**
```ts
React.useState<SettingsSectionType>(cloudfree.defaultSettingsSection)
```

### `src/components/SettingsPage.tsx` (+7 lines)

**Add import near the top:**
```ts
import { CloudFreeNetworkPanel } from "../cloudfree/ui-hooks";
```

**Add `"cloudfree"` to the `SettingsSectionType` union:**
```ts
| "agentMode"
| "cloudfree";
```

**Add case in `renderSectionContent()` switch, before `default`:**
```ts
case "cloudfree":
return <CloudFreeNetworkPanel />;
```

### `src/components/ui/SupportDropdown.tsx` (+4 lines changed)

**Add import:**
```ts
import CloudFreeSupportDropdown from "../../cloudfree/CloudFreeSupportDropdown";
```

**Remove `export default` from the upstream `SupportDropdown` function declaration:**
```ts
// was: export default function SupportDropdown(...)
function SupportDropdown(...)
```

**Add at end of file:**
```ts
// CloudFree: export fork-specific support dropdown
export default CloudFreeSupportDropdown;
```

### `src/components/ControlPanelSidebar.tsx` (-2 lines, +4 lines changed)

**Remove the `Blocks` import from lucide-react.**

**Remove the integrations nav item:**
```ts
// Remove: { id: "integrations", label: t("sidebar.integrations"), icon: Blocks },
```

**In the upgrade banner (the `showUpgradeBanner` block), replace 4 content lines:**
```tsx
// Title: {t("sidebar.upgradeTitle")} → Want full features?
// Desc: {t("sidebar.upgradeDescription")} → This is a privacy-hardened fork. For cloud sync, integrations, and Pro features, try the official app.
// onClick: onUpgrade → () => window.electronAPI?.openExternal("https://openwhispr.com")
// Button: {t("sidebar.learnMore")} → Visit OpenWhispr
```

**Remove the account/sign-in section at the bottom of the sidebar** (the divider + user avatar/name/email block). Replace with:
```tsx
{/* CloudFree: hide account section — no upstream auth */}
```

### `src/updater.js` (2 lines changed)

**In `setupAutoUpdater()`, change the `setFeedURL` owner/repo values:**
```js
autoUpdater.setFeedURL({
provider: "github",
owner: "jrschumacher", // was: "OpenWhispr"
repo: "cloudfree-openwhispr", // was: "openwhispr"
private: false,
});
```

### `electron-builder.json` (+2 lines)

**Add to the `files` array (before the `!node_modules` exclusions):**
```json
"src/cloudfree/**/*",
"cloudfree-allowlist.json",
```

### `src/index.html` (-3 lines, +1 line)

**Replace the 3 Google Fonts CDN lines with:**
```html
<link rel="stylesheet" href="./assets/fonts/noto-sans.css">
```

## Allowlist Format

`cloudfree-allowlist.json` uses v2 format with per-domain path wildcards:

```json
{
"version": 2,
"rules": {
"category_name": {
"_comment": "description",
"entries": [
{ "domain": "api.example.com", "paths": ["/v1/endpoint", "/v1/wildcard/*"] }
]
}
}
}
```

Paths use `*` as wildcard (converted to `.*` regex). Everything not in the allowlist is implicitly blocked. Internal requests (localhost, devtools, file://) are always allowed and tracked separately.

## After an Upstream Merge

1. Re-apply the modifications listed above (most are additive — imports + small patches)
2. If `SettingsSectionType` changed, ensure `"cloudfree"` is still in the union
3. If new settings sections were added upstream, they'll pass through `cloudfree.settingsMap` automatically
4. Run `node scripts/cloudfree/scan-network-calls.js --diff main` to check for new network URLs
5. Update `cloudfree-allowlist.json` if legitimate new endpoints were added upstream
Loading
Loading