Skip to content

fix(privacy): danger zone counts store requests, and the scrub drops the pasted URL - #228

Open
BSalaeddin wants to merge 1 commit into
feat/site-suggestions-delete-my-data-scrubfrom
feat/site-suggestions-delete-reachable-and-raw-url
Open

BSalaeddin wants to merge 1 commit into
feat/site-suggestions-delete-my-data-scrubfrom
feat/site-suggestions-delete-reachable-and-raw-url

Conversation

@BSalaeddin

Copy link
Copy Markdown
Collaborator

Fourth in the stack: #225 -> #226 -> #227 -> this. Base is feat/site-suggestions-delete-my-data-scrub, not dev.

Two follow-ups from #227's report.

1. The danger zone can now be reached by the user it was written for

#227 made "Delete my data" scrub the requester identity off site_suggestions, and the button that reaches it was disabled whenever savings, followed stores and coupon reports were all zero. A request made while signed out carries nothing but the email typed into the form, so the one user whose only personal data is that email read "Nothing to delete" and could never reach the route that removes it.

GET /api/account/overview now returns siteSuggestions.identifyingCount, and the gate counts it.

The count and the scrub are one predicate, not two. Both call siteSuggestionIdentityWhere from the new leaf src/lib/siteSuggestionIdentity.ts (types from @prisma/client and nothing else). Two hand-written copies would let the page say "Nothing to delete" about rows the delete route would happily have scrubbed — and both readings look fine in isolation.

No second "has anything left to scrub" condition. The spec asked for one, and it would be dead code: every branch of the identity predicate requires a non-null column, so a row already scrubbed (user id, email and user agent all null) matches neither branch, and an anonymous row carrying only a user_agent belongs to nobody and is not matched either. Rather than add a guard that reads as a second condition, both facts are pinned — including on real Postgres, where the count goes 1 -> 0 across an actual delete with the row still present.

Copy stays honest. Store requests are deliberately not in the dialog's "This permanently removes ..." list, because the request is not removed. They get their own sentence: "It also takes your email and device details off 2 store requests you sent — the requests themselves stay, just without you attached." A suggestion-only user's dialog never says "permanently removes" at all, and a user with no requests sees no sentence about them.

2. raw_url is scrubbed too

Owner call. A pasted URL can carry a session or affiliate token, and the pipeline only ever keys on domain.

Emptied, not nulled: the column is NOT NULL and rawUrl is a required string on the wire the coupons repo reads, so making it nullable would be a cross-repo contract change for no gain. The empty string is unambiguous — the suggest route's body schema requires at least one character, so no live row can be born with one. domain and status survive; nothing new is stamped.

Verification

gate result
tsc --noEmit clean
app unit 794 passed (was 781)
extension unit 894 passed
integration (real Postgres) 42 passed (was 40)
eslint / oxlint / knip / turbo run prettier-check clean

Red-proofed in three arms:

arm result
the gate drops the suggestion count (the pre-change predicate) reds 4 danger-zone pins
the scrub keeps raw_url reds 4 unit + 2 real-Postgres pins
the count drops the email branch, so the two predicates disagree reds 3 unit + 2 real-Postgres pins

The danger-zone pins render the real section against a real ProfileOverview, and include the pre-change gate kept verbatim so the pair is a pair. ConfirmDialog is built on the native <dialog>, whose showModal()/close() jsdom does not implement; those are stubbed to the open attribute a browser sets, so the component's own effect still runs and the copy is queryable.

Note for the reviewer

ProfileOverview gained a required field, so every construction site had to supply it. That is the point of the shared type, but it does mean this PR touches src/lib/profile/types.ts, which other profile work also lives in.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant