Skip to content

Revival: secure, deployable, and maintainable again (waves 1–4)#199

Open
epodol wants to merge 36 commits into
mainfrom
revival
Open

Revival: secure, deployable, and maintainable again (waves 1–4)#199
epodol wants to merge 36 commits into
mainfrom
revival

Conversation

@epodol

@epodol epodol commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Vibe: https://youtu.be/QWIcz3ab358 🎶

Revives the project after ~3 years of dormancy. The audited plan lives in changelog/2026-07-04-revival-roadmap.md; this PR is the single integration branch for all of it. Kept as a draft until the waves below land.

Why

  • Deploys were broken: Cloud Functions pinned Node 16, a decommissioned GCF runtime, so every release failed.
  • The AI feature was dead in prod: all OpenAI-backed functions called shut-down models/endpoints (Datamuse fallback kept idea generation limping).
  • A confirmed Firestore rules vulnerability: any public map — including view-only — can be modified, and ownership can be taken over, by any request that includes permissions.canPublicEdit: true in its payload.
  • 113 root + 36 functions npm audit findings, almost all via dead react-scripts.

Landed so far

  • Merge release-0.2.0 (CRA → Vite); react-scripts fully gone
  • Node 24 everywhere (functions engines, CI, .nvmrc, CONTRIBUTING)
  • CI/CD modernization: actions/*@v4, archived/unpinned actions replaced, deploys via firebase-tools directly
  • Dependency overhaul: firebase-functions 7 + firebase-admin 13, TS 5.9, Vite 8 / Vitest 4, react-router-dom 7, react-zoom-pan-pinch back on upstream v4 (MUI and React went to 5.18/18.3 here, then to 9/19 in Wave 3)
  • Dead code removed: ai.ts, gpt3_parent.ts, cowsay, dotenv, peer-override blocks
  • Audit baseline: root 113 → 18, functions 36 → 15; after Wave 3's firebase 12 bump the root is at 11, all transitive through dev-only firebase-tools (rest are deliberate holds, see roadmap)
  • Repo hygiene: 24 stale snyk-* branches deleted; backlog triaged into [SECURITY] Firestore rules: public maps can be hijacked — fix update/create rules and add rules unit tests #188Local/session undo for the mind-map editor #198

Wave 1 — security & unblockers (done)

Wave 2 — write model & AI (done)

Wave 3 — remaining stack risk (done)

  • Replace reactfire with custom hooks and upgrade firebase SDK 9 -> 12 #191 (d0597dd) reactfire (dead since 2023) removed → src/firebase/ module: singleton SDK init + the handful of hooks the app uses (useUser/useSigninCheck over one onIdTokenChanged subscription; Firestore doc/collection hooks over onSnapshot with queryEqual-stable resubscription). firebase 9 → 12, @firebase/rules-unit-testing 2 → 5 (lockfile regenerated). Riders: unused Storage/Remote Config inits + remoteconfig.template.json deleted; MindMap rules-of-hooks bug fixed via loader/LoadedMindMap split — missing/denied docs now show the friendly error instead of a raw TypeError.
  • Evaluate and migrate MUI 5 -> latest major #193 (1630982) MUI 5 → 9 + React 18.3 → 19.2 bundled. Go decision: MUI skipped v8, and v9 still defaults to emotion (pigment-css optional) with React 19 support, so we took the latest major directly. @mui/codemod deprecations/all + manual fixes (Menu PaperProps, InputBase inputProps, nine legacy Grid item xs/smsize); React 19 needed one code change (useRef initial value).

Wave 4 — product (done)

  • Export mind maps as PNG, SVG, and Markdown outline #197 (c0ceaf7) PNG/SVG/Markdown export from a new menu in the map bottom bar. SVG clones the live canvas element and crops to the full map via getBBox() (not just the viewport), inlining background/font so the file stands alone; PNG rasterizes that SVG at 2x (capped 4096px); Markdown walks the tree into an indented outline with cycle protection and orphan recovery. Read-only — works on view-only maps.
  • Local/session undo for the mind-map editor #198 (3e171af) Session-local undo on Ctrl/Cmd+Z. Each committed transaction records a per-node before/after diff onto an in-memory stack (max 100, cleared on refresh); undo inverts one entry in a new transaction, and every piece applies only if the node still matches the state the recorded op left it in — newer remote edits win, conflicted pieces are skipped, never resurrected. No redo. Documented in the help dialog.
  • Privacy policy page and site footer #196 (4791c69) /privacy page + site footer. Policy drafted from the live data inventory (auth incl. auto-created anonymous guests, Firestore content/edit history, Groq + Datamuse suggestion calls, Analytics/Perf, App Check reCAPTCHA v3, COPPA posture). Footer (privacy/GitHub/contact/award) on every Navigation route, deliberately absent on the map canvas. Policy text is a DRAFT — see checklist.

All four waves have landed. What remains before un-drafting is the human-only checklist below.

Human-only checklist (not automatable from this branch)

  • Manual click-through of the canvas (pan/zoom/drag/hotkeys) after the zoom-fork migration — now also covers the Wave 3 visual pass (all routes on MUI 9 + React 19: theme toggle, dialogs, Account grids; post-reactfire auth/data flows: anonymous sign-in, claim-account snackbar, list loading states) and Wave 4: the three export formats on a real map, undo across add/edit/delete incl. a two-tab conflict check, footer present on marketing routes / absent on the canvas
  • Create a Groq API key and run npx firebase-tools functions:secrets:set GROQ_API_KEY (then OPENAI_SECRET can be destroyed)
  • Firebase console: verify App Check enforcement is ON for Firestore + API-key HTTP-referrer restrictions on both projects
  • Read and edit the privacy policy draft (src/pages/PrivacyPolicy/index.tsx) — it makes operational commitments (deletion requests via support@bubblemap.app, anonymous-account cleanup, COPPA handling) a human must stand behind; counsel review if being careful. Record the review here before un-drafting.
  • Firebase console: enable auto-deletion of stale anonymous accounts (Authentication → Settings → User account management) — the policy draft now promises it

🤖 Generated with Claude Code

epodol and others added 12 commits July 17, 2023 08:59
Remove Unused ai Function
Add Protections For when openai_key is undefined
Update Dependencies
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- actions/checkout, setup-node, cache: v2 -> v4
- Node 16 -> 20 in all three workflows and CONTRIBUTING.md
- Replace manual npm cache steps with setup-node's built-in npm cache
- Replace archived actions/upload-release-asset with softprops/action-gh-release@v2
- Replace w9jds/firebase-action@master (unpinned third-party action) with
  direct firebase-tools deploy using GOOGLE_APPLICATION_CREDENTIALS
- Drop obsolete FIREBASE_CLI_PREVIEWS env flag from preview workflow

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	.github/CONTRIBUTING.md
#	.github/workflows/cd.yml
#	.github/workflows/ci.yml
#	.github/workflows/preview.yml
- Functions runtime 18 -> 24 (max GA runtime on Cloud Functions; 20 is
  already past its deprecation date)
- firebase-functions 4 -> 7, firebase-admin 11 -> 13 (v7 peer cap),
  typescript 5.9; imports moved to firebase-functions/v1 subpath
- Delete dead gpt3_parent function (never called by client, used retired
  text-davinci-002); remove unused cowsay dep
- CI/CD workflows and CONTRIBUTING.md updated to Node 24; add .nvmrc
- Fix pre-existing lint/format drift from the Vite branch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- npm update across the board (MUI 5.18, router 6.30, react 18.3,
  emotion, eslint plugins, prettier 3.9)
- typescript 5.9.3 (root + functions), @types/node 24 in both packages,
  functions tsconfig target es2017 -> es2023
- firebase-tools 12 -> 15 (required for nodejs24 deploys)
- eslint-config-prettier 8 -> 9
- Remove dotenv (never imported; Vite loads .env itself)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- vite 4 -> 8, @vitejs/plugin-react 4 -> 6, vitest 0.33 -> 4,
  vite-tsconfig-paths 4 -> 6, vite-plugin-pwa 0.16 -> 1.3, jsdom 22 -> 29
- Teach eslint-plugin-import about ESM-only vite packages
  (import/core-modules) since its node resolver can't follow exports maps

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…upstream

- react-router-dom 6.30 -> 7.18 (component APIs unchanged in our usage)
- Replace @kokarn/react-zoom-pan-pinch 2.1.5 (unmaintained fork) with
  upstream react-zoom-pan-pinch 3.7: context.state -> context.transformState,
  setTransform now comes from useControls(); getContext() shimmed to keep
  the parent resetCanvas call shape
- Drop both react-18 peer overrides (upstream supports react 18 natively)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pan-pinch 4

react-zoom-pan-pinch 4 renamed the instance's transformState back to
state (matching the old fork), so the migration shim simplifies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Visit the preview URL for this PR (updated for commit 1ff7df2):

https://mind-map-testing--pr199-revival-2m3jalm0.web.app

(expires Tue, 14 Jul 2026 04:43:27 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: 37740e843bbe4e42b860dff5fdcfb0718403cafd

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread changelog/2026-07-04-revival-roadmap.md
@epodol
epodol marked this pull request as draft July 6, 2026 20:26
… feedback)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@epodol epodol changed the title Revival: Node 24, Vite 8 stack, CI/CD modernization, dependency overhaul Revival: secure, deployable, and maintainable again (waves 1–4) Jul 6, 2026
epodol and others added 8 commits July 6, 2026 13:33
The update rule read permissions.canPublicEdit from the incoming write
(request.resource.data — attacker-controlled) instead of the existing
document, so any public map, including view-only ones, could be modified
or fully taken over by any request that smuggled canPublicEdit: true.

New rules: public-edit eligibility comes from resource.data; every read
and write path requires auth (the app always signs users in, at minimum
anonymously); non-owners cannot touch permissions; create must set
permissions.owner, metadata.createdBy, and everUpdatedBy to the caller's
uid; document schema is validated (allowed keys + types) on every write;
owners cannot transfer ownership (no UI exists for it).

Tests (@firebase/rules-unit-testing v2, pinned to match the firebase 9
client hold — bump to v5 with #191) cover the exploit scenarios plus all
legitimate app write/read/query shapes; they run under npm run test via
emulators:exec and skip when no emulator is available.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…189)

The values are Firebase Web SDK client config — public identifiers that
ship in the JS bundle, not secrets — so .env.example carries the real
values with a header explaining that, and cd.yml/preview.yml copy it to
.env before building (Vite reads .env at build time; without this,
deployed builds would get undefined config). Fork users substitute their
own project config. Also adds a dev-setup section to the README.

Remaining human step from #189: verify API-key HTTP-referrer
restrictions in the Google Cloud console for both projects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
react-hotkeys has been unmaintained since ~2020 and is the likely root
cause of #147 (GENERATE_IDEAS / TOGGLE_SIDE_MENU not firing). All
bindings now live in src/pages/MindMap/keybindings.ts and are wired with
useHotkeys per component; the GlobalHotKeys wrapper elements are gone.

Binding strings are matched against KeyboardEvent.code in v5, hence
'backquote' for the old '`' and 'delete' for 'del'. Callbacks passed
without a deps array are kept fresh via an internal ref, and
enableOnFormTags defaults to false, so hotkeys still don't fire while
typing in inputs/dialogs.

Needs the manual canvas click-through (already on the human checklist)
to confirm #147 is fixed before closing it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Decision: keep PWA support. vite-plugin-pwa (already installed) now has
a real config — registerType autoUpdate (no update-prompt UI exists, so
silent refresh) and the web manifest moved from public/manifest.json
into the plugin config, which injects the link tag itself.

Deleted dead code: public/service-worker.js, src/serviceWorker.js
(registration was already commented out post-Vite), public/offline.html
and public/manifest.json.

Returning-user takeover: the old CRA-era worker was network-first for
navigations (offline fallback only), so returning users fetch the new
HTML, registerSW.js re-registers /sw.js at the same scope and replaces
the old registration; index.tsx deletes the orphaned 'offline' cache.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…der Vite)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ff notes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Node IDs were minted client-side as max(id)+1 from the local snapshot,
and edits were arrayRemove(oldCopy)+arrayUnion(newCopy) batches - both
break under concurrent editing (duplicate IDs, silently duplicated or
resurrected nodes when the local copy is stale).

- node.id/parent are now strings; new nodes get crypto.randomUUID().
  The root keeps the well-known ID '0' so legacy maps (numeric IDs,
  root 0) stay valid: normalizeNodes() coerces numbers to strings on
  read, and the first edit persists the normalized list.
- All node ops run through one runTransaction wrapper: read a fresh
  doc, transform the normalized node list with pure helpers
  (src/nodeOps.ts), write the whole list back. Concurrent-edit
  semantics: updates of deleted nodes no-op instead of resurrecting;
  adds under a deleted parent reattach to the root.
- 16 vitest unit tests for the pure helpers, plus 3 rules tests
  locking in the exact transaction write shape (nodes list + dot-path
  metadata + arrayUnion) for owner/public-editor/view-only.

The rules validate nodes only as a <=5000-entry list, so no rules
change is needed for the ID type change. Undo (#198) is now unblocked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gpt3.ts called createCompletion with gpt-3.5-turbo (a chat model on the
legacy completions endpoint) - broken in prod for years. Swap to Groq's
OpenAI-compatible chat endpoint via plain fetch (Node 24 global), which
also deletes the openai@3 dependency; no SDK, no provider abstraction.

- One shared app-level GROQ_API_KEY Functions secret, same pattern as
  the old OPENAI_SECRET. Deploy needs a human to run
  `npx firebase-tools functions:secrets:set GROQ_API_KEY`.
- Degrades gracefully: missing key, bad input, quota bursts, or API
  errors all log and return [], and the panel falls back to Datamuse.
  Emulator-verified: no-key -> [], invalid key -> real Groq 401 -> [].
- The callable keeps its deployed name `gpt3` (renaming needs a client
  change plus an interactive function-delete on deploy); the panel
  header now says "AI Ideas" instead of "GPT3".
- datamuse.ts never used the OpenAI secret; drop its runWith(secrets)
  so nothing references OPENAI_SECRET anymore.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
epodol and others added 3 commits July 6, 2026 20:18
reactfire has been dead since Aug 2023 and pinned the client SDK to
firebase 9. Replace it with src/firebase/: module-level singleton SDK
init (app, App Check, auth, firestore, functions, emulator wiring) plus
the small hook surface the app actually uses — useAuth/useFirestore/
useFunctions (instance getters), useUser/useSigninCheck (one app-wide
onIdTokenChanged subscription behind FirebaseUserProvider, which blocks
render until the initial auth state is known), and
useFirestoreDocData/useFirestoreCollection/useFirestoreCollectionData
(onSnapshot with a { status, data } shape; queryEqual-stable
resubscription so callers can build queries inline).

Also, per the roadmap notes for this pass:
- Delete the unused Storage and Remote Config inits (nothing ever read
  them; remoteconfig.template.json was an empty {}), and the firebase.json
  remoteconfig block.
- Fix the rules-of-hooks bug in MindMap by splitting it into a loader
  (subscribes to the doc, shows Loading) and LoadedMindMap (all hooks
  unconditional); a missing or permission-denied doc now surfaces the
  intended friendly message instead of a raw TypeError.
- Navigation/ManageMindMaps handle the now-explicit collection loading
  state instead of relying on reactfire suspense.
- firebase 9 -> 12, @firebase/rules-unit-testing 2 -> 5 (lockfile
  regenerated; the old lock still carried reactfire's ^9 pin).

All 41 tests (rules + nodeOps) pass under the Firestore emulator; tsc,
eslint, prettier, and the production build are clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Go decision: MUI skipped v8; v9 (9.2) still defaults to emotion
(pigment-css peer is optional) and supports React 19, so we take the
latest major directly rather than parking on 6/7. Actual churn was
small, confirmed by inventory before migrating:

- `@mui/codemod deprecations/all` converted TextField inputProps/
  InputProps -> slotProps.htmlInput/input, Snackbar TransitionProps ->
  slotProps.transition, ListItemText primaryTypographyProps ->
  slotProps.primary, Drawer PaperProps -> slotProps.paper.
- Manual: Menu PaperProps and InputBase inputProps (codemod missed
  them), and the nine legacy `Grid item xs/sm` usages -> the v9 Grid
  `size` prop (GridLegacy is removed in v9).
- React 19: @types/react 19 requires an initial value for useRef
  (MindMapSimulation childRef); everything else was already compatible
  (createRoot, no defaultProps, no legacy context).

react-zoom-pan-pinch 4, react-router-dom 7, react-hotkeys-hook 5, and
testing-library 16 all declare React 19 peer support. tsc, eslint,
prettier, the 41-test emulator suite, and the production build are all
green. Visual smoke check of the routes rides on the existing manual
click-through item on the human checklist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… + React 19)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
epodol and others added 4 commits July 6, 2026 20:43
Export menu in the map bottom bar. SVG export clones the live canvas
element, crops it to the full map extent via getBBox, and inlines
background/font so the file stands alone; PNG rasterizes that SVG at 2x
(capped at 4096px). Markdown walks the tree from the root into an
indented outline, with cycle protection and orphan recovery. Read-only:
works on view-only maps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each committed transaction records a per-node before/after diff
(diffNodes) onto an in-memory stack (max 100, cleared on refresh).
Ctrl/Cmd+Z pops one entry and inverts it in a new transaction via
withChangesUndone, where every piece applies only if the node still
looks the way the recorded operation left it — a newer remote edit
wins and the conflicting piece is skipped, never resurrected.
Undoing a delete re-adds the node and restores its reparented
children; undo transactions are not themselves recorded (no redo).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/privacy covers the actual data inventory: Firebase Auth incl.
auto-created anonymous guest accounts, Firestore map content and edit
history, Groq/Datamuse suggestion calls, Analytics/Performance
Monitoring, App Check reCAPTCHA v3, and the COPPA posture (no ads, no
selling, parent contact). Footer (privacy/GitHub/contact links + App
Challenge award) follows the Navigation pattern: present on Home,
About, Privacy, SignIn, CreateAccount, Account, ManageMindMaps; absent
on the map canvas.

Policy text is a DRAFT: human review required before production
deploy, and anonymous-account auto-cleanup must be enabled in the
Firebase console.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cy + footer)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
epodol and others added 5 commits July 6, 2026 21:06
…promise (#196)

Human-review pass on the draft: replace the "we do not show ads" claim
with a commitment that ads (if any ever appear) receive no user data;
document the existing in-app Delete Account flow and that it does not
cascade to maps; drop the self-imposed promise to announce changes on
the home page. Also document VITE_RECAPTCHA_PUBLIC_KEY in .env.example —
CD builds copy that file verbatim, so App Check never initializes in
production until the real site key is committed there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep the existing hero (logo, title, tagline, one primary CTA) and add
the standard landing sections below it: award social proof, six feature
blurbs (including Wave 4 export), a three-step how-it-works, an FAQ
accordion group with matching FAQPage JSON-LD, and a closing CTA. The
CTA now disables while the anonymous sign-in/map creation is in flight,
the hero uses the 38KB SVG logo instead of the 367KB PNG, and the page
is semantic (single h1, sequential h2/h3, main landmark).

SEO surface: descriptive <title>, richer meta description, canonical
URL, WebApplication JSON-LD replacing the orphaned itemprop microdata,
URL-encoded og/twitter image URLs, plus robots.txt (disallow /mindmaps
and /account) and sitemap.xml.

Verified end-to-end with Playwright against the emulators: all sections
render light/dark/mobile with no console errors, and the hero CTA
creates a guest account and lands on a working canvas. Recipe persisted
in .claude/skills/verify.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Public client identifier; CD copies .env.example into .env at build
time, so this makes production builds initialize App Check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@epodol
epodol marked this pull request as ready for review July 7, 2026 04:48
@epodol
epodol requested a review from SpiritSeal July 7, 2026 04:48
@epodol epodol self-assigned this Jul 7, 2026
@epodol epodol added enhancement New feature or request Core Core Functionality. Highest Priority architecture labels Jul 7, 2026
@SpiritSeal

SpiritSeal commented Jul 7, 2026

Copy link
Copy Markdown
Owner
image

Oh boy

@SpiritSeal SpiritSeal left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few things:

  • Let's add tests across all of this code we are adding. Generally, I'm hoping for ~80% code coverage on business logic and >70% coverage on UI and everything else. Use techniques like dependency injection to model for nondeterministic components like the LLM functions.
  • I see that we are changing some of the GCP products we are using. Could I get an updated cost analysis?
  • Let's talk about deleting stale anon accounts. Do we really want to do this for now? It's not really costing us anything to keep them at our current scale. I'm probably fine with the code we have written for it (haven't read it yet), but may be worth && falseing it for now until we have a business need to activate it
  • Could we have an updated live preview URL for these changes
  • Let's have an LLM do a rudimentary security model and security analysis of our app and make sure there aren't any gaping holes (post results mb in changelog or new similarly styled dir?). Would p also be worth adding a SECURITY.md and security policy to bubblemap in case a security researcher finds something and would like to disclose it.
  • Generally going forward, let's try to keep 1 PR as 1 atomic reviewable unit of code (i.e. 1 new feature)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

architecture Core Core Functionality. Highest Priority enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GENERATE_IDEAS & TOGGLE_SIDE_MENU Broken

2 participants