Tappymaps revamp: full-app audit + fixes - #41
Merged
Conversation
…d quiet The committed smoke test served the repo over `python -m http.server` (no Vercel rewrites, no /api) and ignored every console error matching /net::ERR|Failed to load US map|census|topojson/. On a machine that cannot reach the CDN it printed "all 10 routes booted clean" while ZERO state paths had rendered and chroma / topojson / dom-to-image / html2canvas / supabase were all undefined. Reproduced and confirmed before rewriting. A green smoke run on a dead app is worse than no smoke run, so: - add scripts/devserver.mjs — a local Vercel-alike server that reproduces vercel.json (SPA rewrite, /s/:hash, /api/* stubs) and vendors the six third-party <script> tags plus both us-atlas TopoJSON files into .cache/vendor/. Hermetic after first run, and a missing vendor file is a hard 500 rather than a silent network error a test can ignore. - rewrite scripts/smoke.mjs around positive functional assertions instead of error-absence: libraries actually loaded, 51 state paths rendered, a click colors a state and a second click toggles it off, captureMapImage returns a non-blank canvas at real dimensions, every route resolves to a mode, the arcade run fills its prompt queue and reproduces from a seed, and GeoDraft opens in the pick phase having ranked all 50 states. Both games are checked to render their own 51-state SVG. - narrow the ignore list to favicon and the two stubbed endpoints. 24 assertions, all passing. `npm run dev` now serves the app the way production does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmhvochBpXL4rpuccjAQx9
Now that smoke.mjs makes real functional assertions and runs hermetically through scripts/devserver.mjs, it is worth gating on: ~10s locally. Validate alone only proves the file parses, which is blind to the runtime TDZ class of bug that blanks the whole app. Adds a second job that installs Chromium, caches the vendored libs, and runs the smoke suite. Bumps the Node version to 22 to match local development. Whether these checks block merges is a branch-protection setting on the repo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmhvochBpXL4rpuccjAQx9
Adversarially tested the new smoke suite by injecting two regressions. It caught the runtime TDZ (a top-level read of fipsToState from the Arcade block): 13 failed checks, exit 1, while `validate` still reported PASS — which is exactly the blind spot the smoke test exists to cover. It did NOT catch a silently hidden legend inside captureMapImage. Every export check still passed, because a distinct-colour heuristic is far too coarse to notice a missing legend — the kind of regression that looks fine in the editor and only shows up in the downloaded file. So the rasterizer entry points are now wrapped to snapshot computed style and bounding box at the exact moment the node is handed over, asserting the logo, map title, and legend are all genuinely visible in the captured output. The logo check encodes the Phase 0 §9 rule that it is mandatory on every export for every tier. Re-ran the injected regression: it now fails as it should. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmhvochBpXL4rpuccjAQx9
Closes a long-standing open item from HANDOVER.md: CRLF/LF warnings fired on every edit from a Windows checkout. Cosmetic, but it made `git status` noisy enough to hide real changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmhvochBpXL4rpuccjAQx9
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
CI went red on /design/gallery/recent while the same route passed locally. The difference was reachability: GitHub Actions can reach Supabase and got a 404 back; locally the request simply never completed. A suite that flips on a third party's uptime gets ignored, so everything leaving localhost is now intercepted and fulfilled with an empty-but-valid response. Fulfil rather than abort — an aborted request logs its own console error, which the suite would then have to special-case away, and a broad "ignore network errors" rule is precisely what made the previous version blind. Blocked origins are printed as an informational footer, never a failure, so a newly added runtime dependency is still visible in the log. The 404 that surfaced this is a real product bug and is being tracked separately: the client queries user_maps, map_reports, gallery_publish_counts and classroom_codes, and none of those tables exist in the live database. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmhvochBpXL4rpuccjAQx9
checkExportPermission() and recordExport() both read `window._supabase`,
which is never assigned anywhere in the file — the client is `supabaseClient`,
declared above them, and that is the idiom every other call site uses.
So the first line of each try block threw a TypeError, the catch returned
`{allowed:false, serverError:true}`, and every signed-in free user was told
"check your connection" and blocked from exporting. /api/stripe/track-export
was never reached, which means the server-enforced 3-exports-per-month quota
has never actually run in production.
Measured, before and after, driving the real app:
pre-fix checkExportPermission -> {allowed:false, serverError:true}
track-export requests -> NONE
post-fix checkExportPermission -> {allowed:true, remaining:3}
track-export requests -> POST /api/stripe/track-export
Both call sites now use supabaseClient and bail out cleanly when it is null
(the Supabase CDN script failing to load should not read as a server error).
Nothing exercised the signed-in export path, which is why this survived, so
the smoke suite now asserts the quota endpoint is genuinely reached. Verified
the new check fails against the pre-fix code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmhvochBpXL4rpuccjAQx9
…nd dropped checkouts The money path had no tests. Adding some found three real defects and one bug in the first attempt at fixing them. verify-subscription - A stored row was treated as proof of entitlement forever. One dropped customer.subscription.deleted webhook meant permanent free Pro. The row is now authoritative only while the period it describes is still running, plus a 3-day grace window for clock skew and late renewal webhooks. Past that, it reconciles against Stripe and repairs the row, so a dropped webhook costs one extra API call instead of an unbounded free subscription. - past_due was excluded from the entitling statuses, so the first failed charge revoked Pro mid-period — for a period the customer had already paid for. It now entitles through the dunning window; the period-end check ends access. - Reconciliation fails OPEN on a Stripe outage. A third party having a bad minute must not lock out a paying customer. A genuine 404 still revokes. webhook - getRawBody used `data += chunk`, decoding each chunk independently as UTF-8. A multi-byte character straddling a chunk boundary corrupted the body so it no longer matched the bytes Stripe signed. Measured across every split offset of a payload containing an accented name: corrupts at 8 of 35 (22.9%). Now collects Buffers and concatenates. The test scans every offset — a single hand-picked boundary passes against the broken version, which is how this presented as a flaky webhook rather than an obvious break. - checkout.session.completed without client_reference_id hit `break` and returned 200, telling Stripe the event was handled and permanently dropping a subscription the customer had paid for. It now attempts recovery via stripe_customer_id and otherwise returns 500 so Stripe retries and the failure is visible. Writing the tests also surfaced that a missing STRIPE_SECRET_KEY makes the new reconcile path silently fail open; the handler now warns at startup. 11 tests, wired into `npm test` and a CI job. No production calls — Supabase and Stripe are stubbed through a module loader hook so the handlers run unmodified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmhvochBpXL4rpuccjAQx9
mapzimus
marked this pull request as ready for review
August 21, 2026 21:55
mapzimus
pushed a commit
that referenced
this pull request
Aug 21, 2026
The 169 findings existed only in an ephemeral scratchpad. They are the most reusable output of this work — every one reproduced in a browser, with repro steps, evidence and a suggested fix — so they belong in the repo. docs/audits/2026-08-21/ holds the eight per-domain reports (3,887 lines), the consolidated HTML summary, and a README with the severity table, the P0 fixed/open split, and the two claims that were investigated and withdrawn during the audit (the quick-fill double-binding, and the 744KB payload — it is 174KB gzipped) so they are not re-reported later. HANDOVER.md now leads with this pass: what shipped in #41 and #42, what the rewrite foundation under src/ actually is (data extracted and verified byte-identical, one shared renderer, a hardened URL codec — not yet an application), and the three things blocked on the owner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BmhvochBpXL4rpuccjAQx9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
A full-scale audit of Tappymaps followed by fixes. Ten agents drove the real app in a real browser — Create editor, export pipeline, Arcade, GeoDraft, Gallery/Embed/Share, monetization + serverless, mobile/responsive, a11y/SEO/perf, code health + security, and data maps/CSV import — and the findings are being fixed on this branch.
Status: audit in progress. This PR is open early so the work is durable. It will be updated as findings land and fixes ship.
Landed so far — the safety net
The project's own smoke test was giving false green. Reproduced before touching it:
It served the repo over
python -m http.server(no Vercel rewrites, no/api) and its ignore list contained a broadnet::ERR, so a total CDN failure read as a clean boot.scripts/devserver.mjs(new) — local Vercel-alike server reproducingvercel.json: SPA rewrite,/s/:hash,/api/*stubs. Vendors the six third-party<script>tags and both us-atlas TopoJSON files into.cache/vendor/, so the harness is hermetic and a missing asset is a hard 500 rather than an ignorable network error.npm run devnow serves the app the way production does.scripts/smoke.mjs(rewritten) — 24 positive functional assertions instead of error-absence: libraries loaded, 51 state paths rendered, a click colors a state and a second click toggles it off,captureMapImagereturns a non-blank canvas at real dimensions, every route resolves to a mode, the Arcade run fills its prompt queue and reproduces from a seed, GeoDraft opens in the pick phase having ranked all 50 states, both games render their own 51-state SVG..gitattributes— closes the long-standing CRLF/LF open item.The suite was adversarially tested
Injected a runtime TDZ (a top-level read of
fipsToStatefrom the Arcade block): 13 failed checks, exit 1, whilevalidatestill reportedPASS— exactly the blind spot smoke exists to cover.Injected a silently hidden legend inside
captureMapImage: it passed, because a distinct-colour heuristic can't see a missing legend. That gap is why the capture-time invariant checks were added; the same injection now fails as it should.Verification
npm run validateandnpm run smokeboth green on every commit here.🤖 Generated with Claude Code
https://claude.ai/code/session_01BmhvochBpXL4rpuccjAQx9
Generated by Claude Code