{"id":50449628,"url":"https://github.com/paperhurts/gitmarks","last_synced_at":"2026-05-31T23:32:14.857Z","repository":{"id":359888895,"uuid":"1247632379","full_name":"paperhurts/gitmarks","owner":"paperhurts","description":"Serverless cross-browser bookmark sync. Your bookmarks live as a JSON file in your own private GitHub repo; the extensions talk directly to the GitHub Contents API. No server, no backend.","archived":false,"fork":false,"pushed_at":"2026-05-24T01:13:43.000Z","size":201,"stargazers_count":0,"open_issues_count":8,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-24T01:19:26.590Z","etag":null,"topics":["bookmark-sync","chrome-extension","mv3","serverless","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/paperhurts.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-23T15:19:17.000Z","updated_at":"2026-05-24T01:13:47.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/paperhurts/gitmarks","commit_stats":null,"previous_names":["paperhurts/gitmarks"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/paperhurts/gitmarks","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paperhurts%2Fgitmarks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paperhurts%2Fgitmarks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paperhurts%2Fgitmarks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paperhurts%2Fgitmarks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/paperhurts","download_url":"https://codeload.github.com/paperhurts/gitmarks/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paperhurts%2Fgitmarks/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33753923,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bookmark-sync","chrome-extension","mv3","serverless","typescript"],"created_at":"2026-05-31T23:32:08.819Z","updated_at":"2026-05-31T23:32:14.852Z","avatar_url":"https://github.com/paperhurts.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# gitmarks\n\nServerless cross-browser bookmark sync. Bookmarks live as a JSON file in\n**your own private GitHub repo**; browser extensions and a web UI both talk\ndirectly to the GitHub Contents API. No server, no backend, no\ninfrastructure to host. You own your data — it's just a file in a repo\nyou control.\n\n**Status:** Chrome extension is functional end-to-end (save via toolbar\nbutton, two-way sync with the native bookmark tree, 5-min poll for remote\nchanges, automatic conflict retry). Firefox MV3 add-on shipping the same\nsource as Chrome via a shared package. Web UI (list, search, tag management,\nbulk operations, trash, Netscape HTML export, sign out) deploys as a static\nSPA. Safari is next in the roadmap. See `spec.md` for the full design.\n\n## Features (Chrome, today)\n\n- Save the current tab to GitHub via a toolbar button\n- Drag a URL to your Chrome bookmarks bar → it appears in `bookmarks.json`\n  on GitHub within ~1 second\n- Edit a bookmark's title in Chrome → updates remote within ~1 second\n- Delete a bookmark in Chrome → soft-deleted (tombstoned) remotely;\n  garbage-collected from the JSON after 30 days but retained in git\n  history forever\n- Edit `bookmarks.json` directly on GitHub → changes pull into Chrome\n  on the next 5-minute poll\n- Concurrent edits from multiple devices reconcile automatically via\n  GitHub's file SHA + optimistic retry-replay\n- 290 automated unit + component tests + 6 Playwright e2e (against real Chromium)\n- Optional **tracking-param stripping** (utm_*, fbclid, gclid, etc.) at save time — opt-in via settings\n\n## Packages\n\n| Package | Role |\n|---|---|\n| `@gitmarks/core` | Shared TypeScript library: schemas (Zod), GitHub Contents API client with optimistic concurrency, ULID + URL helpers, pure mutation helpers |\n| `@gitmarks/extension-shared` | Cross-browser extension source — popup, options, background, lib/ helpers. Consumed by both browser shells via `workspace:*`. 104 unit tests live here. |\n| `@gitmarks/extension-chrome` | Chrome MV3 shell. Manifest + Vite/crxjs build + Playwright e2e. Thin entry files import from `extension-shared`. |\n| `@gitmarks/extension-firefox` | Firefox MV3 shell. Manifest + plain Vite build. Same source as Chrome via `extension-shared`. Load via `about:debugging`. |\n| `@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. |\n\n## Try the web UI\n\nThe read-side web UI is auto-deployed to GitHub Pages:\n**https://paperhurts.github.io/gitmarks/**\n\nYou'll need a fine-grained PAT (see \"Your data, your PAT\" below) and your\nown private bookmarks repo. The web UI runs entirely in your browser — no\nserver sees your token.\n\n## Quick start (Chrome extension)\n\n```bash\npnpm install\npnpm --filter @gitmarks/extension-chrome build\n```\n\nThen in Chrome:\n1. `chrome://extensions/` → toggle **Developer mode** on\n2. **Load unpacked** → select `packages/extension-chrome/dist/`\n3. Click the toolbar icon → \"Set up gitmarks\"\n4. Paste a fine-grained PAT (Contents: read/write scope on your bookmarks\n   repo), enter owner/repo/branch, click **Save**\n\nSee `packages/extension-chrome/README.md` for the full setup walkthrough,\nthe manual smoke test checklist, and architecture notes.\n\n## Your data, your PAT\n\n- **The repo must be private.** Public repo + the project name = anyone\n  can find your bookmarks. The extension does NOT enforce this — it's\n  on you when you create the repo on github.com.\n- **Use a fine-grained PAT** scoped to *only* your bookmarks repo with\n  *only* Contents: read/write. Never use a classic PAT or one with broader\n  scopes — if your browser profile is ever exfiltrated, that token only\n  unlocks your bookmarks, not your whole GitHub account.\n- **The PAT is stored in `chrome.storage.local`**, which is origin-scoped\n  (other extensions / sites can't read it) but readable by anyone with\n  access to your unlocked browser profile. Treat it like a saved\n  password.\n- **No telemetry.** The extension only talks to `api.github.com`. That's\n  enforced by the MV3 manifest's `host_permissions`.\n\n### PAT lifecycle / revocation\n\nWhen you stop using gitmarks (uninstall the extension, clear browser data, or switch machines):\n\n1. **Revoke the PAT on github.com.** Settings → Developer settings → Personal access tokens → Fine-grained tokens → find the one named for your bookmarks repo → **Delete**. This is the only authoritative way to invalidate the credential.\n2. **Web UI:** click **Sign out** in the header. This clears `localStorage` on your current machine. (It does NOT revoke the token on GitHub — see step 1.)\n3. **Extension:** uninstalling the extension removes its `chrome.storage.local` entry on that machine. The token on GitHub remains valid until you revoke it.\n\nTreat the PAT like a saved password. If a machine is lost or compromised, revoke immediately on github.com.\n\n## Development\n\n```bash\n# Everything\npnpm install\npnpm test           # all unit tests across packages\npnpm typecheck\npnpm build\n\n# Just one package\npnpm --filter @gitmarks/core test\npnpm --filter @gitmarks/extension-shared test   # all extension unit tests live here\npnpm --filter @gitmarks/extension-chrome e2e    # Playwright + real Chromium\n```\n\nThe repo is a pnpm workspace monorepo. Each package has its own\n`README.md` with package-specific docs.\n\n## Architecture\n\n```\n[Chrome ext] [Firefox ext] [Safari ext (planned)]    [Web UI]\n       \\             |                       /                       /\n        \\            |                      /                       /\n         v           v                     v                       v\n                          GitHub REST API (api.github.com)\n                                       |\n                                       v\n                          User's private repo: bookmarks.json + tags.json\n```\n\nThe load-bearing invariants:\n\n- **No server, ever.** Clients talk to GitHub REST API directly. PAT\n  lives client-side (`chrome.storage.local`).\n- **Optimistic concurrency** via GitHub file SHA. On 409, the core\n  client refetches and replays the mutation (up to 3 attempts with\n  exponential backoff).\n- **Eventual consistency, ~30s target.** Event-driven push for local\n  changes (500ms debounce). 5-minute poll for remote changes via\n  `chrome.alarms`, with ETag conditional reads so unchanged polls cost\n  nothing against the rate limit.\n- **Soft deletes** (tombstones) for ~30 days; git history retains\n  everything forever.\n- **Suppression registry** prevents loop-back: when the extension applies\n  a remote change to `chrome.bookmarks`, the affected URL is parked in\n  an in-memory registry for ~2 seconds so the resulting local event\n  doesn't echo back to GitHub.\n\n## Roadmap\n\n- ✅ `@gitmarks/core` — schemas, GitHub client, mutations\n- ✅ Chrome MVP — toolbar-button save flow\n- ✅ Chrome native tree integration — listeners, reconcile, poll loop\n- ✅ Tracking-param stripping (opt-in)\n- ✅ Firefox MV3 add-on ([#23](https://github.com/paperhurts/gitmarks/issues/23))\n- ✅ Web UI v1: list + search + tag management ([#24](https://github.com/paperhurts/gitmarks/issues/24))\n- ✅ Web UI v2: bulk operations + trash + export ([#25](https://github.com/paperhurts/gitmarks/issues/25))\n- ⬜ Safari ([#26](https://github.com/paperhurts/gitmarks/issues/26))\n\n## Files in this repo\n\n- `spec.md` — full design spec (source of truth for non-obvious decisions)\n- `CONTRIBUTING.md` — branch/PR conventions, TDD policy, plan-driven workflow\n- `CLAUDE.md` — guidance for AI agents working in this repo\n- `LICENSE` — MIT\n- `docs/superpowers/plans/` — implementation plans, one per branch\n- `packages/*/README.md` — package-specific documentation\n- `examples/example-bookmarks-repo/` — sample `bookmarks.json` + `tags.json`\n  to seed a fresh repo, used by `@gitmarks/core` fixture tests\n- `.github/workflows/test.yml` — CI (typecheck + unit tests + build on every PR)\n\n## Contributing\n\nSee `CONTRIBUTING.md` for the branch/PR conventions, conventional-commit\nscopes, and the plan-driven workflow used for larger features. Every\nchange goes through a PR with green CI — no direct commits to `main`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpaperhurts%2Fgitmarks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpaperhurts%2Fgitmarks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpaperhurts%2Fgitmarks/lists"}