diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml new file mode 100644 index 0000000..dcee214 --- /dev/null +++ b/.github/workflows/deploy-web.yml @@ -0,0 +1,71 @@ +name: Deploy web UI to GitHub Pages + +on: + # Deploy on every push to main that touches the web package (or this workflow). + push: + branches: [main] + paths: + - "packages/web/**" + - "packages/core/**" + - ".github/workflows/deploy-web.yml" + - "pnpm-lock.yaml" + # Allow manual re-deploy from the Actions UI. + workflow_dispatch: + +# Least-privilege except for the Pages-specific permissions deploy-pages needs. +permissions: + contents: read + pages: write + id-token: write + +# Only one Pages deployment can run at a time. If another commit lands while a +# deploy is in flight, cancel the queued one and skip ahead — never queue +# multiple concurrent deploys for the same env. +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build core + run: pnpm --filter @gitmarks/core build + + - name: Build web + run: pnpm --filter @gitmarks/web build + + - name: Configure GitHub Pages + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: packages/web/dist + + deploy: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 5 + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/CLAUDE.md b/CLAUDE.md index 2d42484..4b2f644 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,12 +6,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Five packages are merged to main and working: - `@gitmarks/core` (`packages/core/`) — schemas, GitHub Contents API client with optimistic concurrency, ULID/URL helpers (incl. opt-in tracking-param stripping), pure mutation helpers, example fixtures. 77 unit tests. -- `@gitmarks/extension-shared` (`packages/extension-shared/`) — canonical owner of the cross-browser extension code: popup, options, background, all of `src/lib/`, and the chrome/browser stub. 100 unit tests live here. Consumed by both browser shells via `workspace:*`. Uses `browser.*` via `webextension-polyfill`. +- `@gitmarks/extension-shared` (`packages/extension-shared/`) — canonical owner of the cross-browser extension code: popup, options, background, all of `src/lib/`, and the chrome/browser stub. 104 unit tests live here. Consumed by both browser shells via `workspace:*`. Uses `browser.*` via `webextension-polyfill`. - `@gitmarks/extension-chrome` (`packages/extension-chrome/`) — Chrome MV3 shell. Manifest + Vite/crxjs build + Playwright e2e (4 passing, 2 skipped — see issue history for the activeTab/Playwright limitation). Source files are thin entries that re-export from `extension-shared` via its `exports` map. - `@gitmarks/extension-firefox` (`packages/extension-firefox/`) — Firefox MV3 shell. Manifest + plain Vite build + manual smoke test (Playwright Firefox doesn't reliably drive WebExtensions). Targets Firefox 121+ for MV3 SW parity. Load via `about:debugging` → "Load Temporary Add-on". - `@gitmarks/web` (`packages/web/`) — Vite + React + Tailwind SPA. List, search, tag management, bulk operations, trash, Netscape HTML export. Talks directly to GitHub via `@gitmarks/core`. Hash routing (`#/setup`, `#/`, `#/tags`, `#/trash`). 109 unit + component tests. -Total: 286 unit + component tests across the monorepo, plus 6 Playwright e2e (4 passing, 2 skipped) in the Chrome shell. +Total: 290 unit + component tests across the monorepo, plus 6 Playwright e2e (4 passing, 2 skipped) in the Chrome shell. The web UI is auto-deployed to GitHub Pages by `.github/workflows/deploy-web.yml` on every push to `main` that touches `packages/web/**` or `packages/core/**`. Pending packages (in dependency order): Safari. @@ -32,10 +32,10 @@ These are spec-level constraints; don't violate without an explicit discussion: - **IDs are ULIDs generated client-side.** Native browser node IDs are not stable across reinstalls — the extension maintains a `{ulid: chrome_node_id}` map in `chrome.storage.local`, rebuilt by URL match. - **Folder ↔ string path:** `Bookmarks Bar` ↔ `""` (root), `Other Bookmarks` ↔ `"_other"`, nested folders joined with `/`. Folder structure is derived from bookmarks, not stored separately. - **Loop suppression:** when applying a remote change to `chrome.bookmarks`, register the URL in an in-memory TTL map for ~2s so our own listener doesn't echo it back to GitHub. -- **URL safety:** Bookmark URLs are checked against an allowlist of safe schemes (`isSafeBookmarkUrl` in `@gitmarks/core`) at three points: (a) save time in the extension's `buildBookmark` factory, (b) render time in the web UI's `BookmarkRow`, and (c) the extension's `apply-remote` boundary that writes remote entries into the native bookmark tree. Unsafe schemes (`javascript:`, `data:`, etc.) are rejected/skipped. +- **URL safety:** Bookmark URLs are checked against an allowlist of safe schemes (`isSafeBookmarkUrl` in `@gitmarks/core`) at every write or render boundary: (a) popup save in `buildBookmark` (throws); (b) the SW listener path in `applyBatch` create + update branches (skip + warn); (c) reconcile's `remoteByUrl` construction (filters unsafe remote entries before they reach the local tree or the idMap); (d) reconcile's `localOnly` upload loop (filters unsafe local entries before pushing to GitHub); (e) the `apply-remote` boundary that writes remote entries into `browser.bookmarks` (skip + warn); (f) the web UI's `BookmarkRow` render (turns unsafe URLs into non-clickable italic spans with a tooltip). Unsafe schemes (`javascript:`, `data:`, etc.) are rejected/skipped everywhere bookmarks cross a trust boundary. - **Remote file validation:** `useGitmarksData` re-validates `bookmarks.json` and `tags.json` through Zod (`bookmarksFileSchema` / `tagsFileSchema`) after reading from GitHub. Malformed remote data surfaces as an error rather than rendering attacker-controlled fields. - **Tag color guard:** `TagChip` regex-validates the color string before placing it into a CSS `style` object; malformed colors fall back to a default. -- **CSP:** Web UI's `index.html` and both extension manifests carry an explicit Content-Security-Policy restricting `connect-src` to `https://api.github.com`, disallowing inline scripts, framing, and form actions. +- **CSP:** Both extension manifests carry an explicit `content_security_policy.extension_pages` restricting `connect-src` to `'self' https://api.github.com`, disallowing inline scripts. The web UI's `` CSP is injected by a Vite plugin scoped to `apply: "build"` only (running it in dev would block Vite's HMR WebSocket); `frame-ancestors` is intentionally omitted because `` cannot enforce it per CSP3 — clickjacking defense must come from an HTTP header at the hosting layer. ## Architecture by package diff --git a/README.md b/README.md index aadab32..5672325 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ SPA. Safari is next in the roadmap. See `spec.md` for the full design. on the next 5-minute poll - Concurrent edits from multiple devices reconcile automatically via GitHub's file SHA + optimistic retry-replay -- 286 automated unit + component tests + 6 Playwright e2e (against real Chromium) +- 290 automated unit + component tests + 6 Playwright e2e (against real Chromium) - Optional **tracking-param stripping** (utm_*, fbclid, gclid, etc.) at save time — opt-in via settings ## Packages @@ -34,11 +34,20 @@ SPA. Safari is next in the roadmap. See `spec.md` for the full design. | Package | Role | |---|---| | `@gitmarks/core` | Shared TypeScript library: schemas (Zod), GitHub Contents API client with optimistic concurrency, ULID + URL helpers, pure mutation helpers | -| `@gitmarks/extension-shared` | Cross-browser extension source — popup, options, background, lib/ helpers. Consumed by both browser shells via `workspace:*`. 100 unit tests live here. | +| `@gitmarks/extension-shared` | Cross-browser extension source — popup, options, background, lib/ helpers. Consumed by both browser shells via `workspace:*`. 104 unit tests live here. | | `@gitmarks/extension-chrome` | Chrome MV3 shell. Manifest + Vite/crxjs build + Playwright e2e. Thin entry files import from `extension-shared`. | | `@gitmarks/extension-firefox` | Firefox MV3 shell. Manifest + plain Vite build. Same source as Chrome via `extension-shared`. Load via `about:debugging`. | | `@gitmarks/web` | Static SPA — list, search, tag management, bulk operations, trash, Netscape HTML export, sign out. Vite + React + Tailwind. Talks directly to GitHub via `@gitmarks/core`. Deploys to GitHub Pages or Cloudflare Pages. | +## Try the web UI + +The read-side web UI is auto-deployed to GitHub Pages: +**https://paperhurts.github.io/gitmarks/** + +You'll need a fine-grained PAT (see "Your data, your PAT" below) and your +own private bookmarks repo. The web UI runs entirely in your browser — no +server sees your token. + ## Quick start (Chrome extension) ```bash