Repository navigation
feat(reporting): share the report reason vocabulary, and sign the target - #1519
Merged
Merged
Conversation
`ReportRequest` has no category field -- only a free-form `description` -- so a report's reason has to be encoded into that string. Encoding it twice, once per platform, gives moderation two spellings of the same report. New `:libs:reporting` holds the six tokens and the rule that assembles them: the token on the first line, the reporter's own words from the second on. It rides the existing `SharedCore` framework, and `ReportReason.swift` re-exports the Kotlin enum rather than restating it, so the tokens are written down once for both apps. `ReportingApi` also set its auth block before the target. `authenticate()` signs `buildPartial()`, a snapshot of the builder at that moment, so every report was signed over its description alone. The auth block now goes last, which is how `BlocklistApi` has always built its requests.
This was referenced Sep 21, 2026
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.
Both apps could already send a report and neither had a way to ask for one. This is the half of that work that is shared: the reason vocabulary, the Swift facade over it, and a signing bug that would have shipped with the first caller.
Design:
docs/superpowers/specs/2026-09-21-reporting-design.mdin the orchestrator repo (under review as code-payments/flipcash-client-orchestrator#16).The vocabulary is shared code, not a convention
flipcash.reporting.v1.ReportRequesthas no category field.descriptionis a free-form string and the only place a reason can go, so the reason has to be encoded into it. Encoding it separately on each platform is the thing that drifts, and a vocabulary that drifts gives moderation two spellings of the same report.New
:libs:reportingholds the six tokens and the assembly rule — token on line one, the reporter's own words from line two on — and is exported through:kmp:shared-coreon theapi(...)andexport(...)lists.ReportReason.swiftre-exports the Kotlin enum under a local name rather than restating it in Swift; a Swift enum with its own six cases would be a second copy of exactly what this module exists to make single.ReportDescriptionTestwrites every expected string out as a literal, so a renamed token or a dropped newline fails the build instead of quietly changing what moderation receives.ReportReasonTestscovers the bridge rather than the behaviour: that the wrap does not reorder or drop anything, that the cap crosses as a number, and thatdisplayOrderis exhaustive — a seventh reason added in Kotlin compiles fine on both sides and would otherwise be silently missing from the iOS sheet.One note on the facade for whoever writes the iOS UI: Kotlin/Native lowercases the whole enum case, so the Swift spellings are
.scamorfraudand.sexualcontent, not the camel-case you would expect from the Kotlin.The signature covered the wrong bytes
ReportingApiset its auth block before the target:authenticate()signsthis.buildPartial()— a snapshot of the builder at the moment it is called — so every report Android sends is signed over itsdescriptionand nothing else. iOS builds target-first and signs both.That makes the rule positional for every API in that package, not a quirk of reporting: any field set after
authenticate()is outside the signature.ReportingApiis the only current violator, andBlocklistApigets the ordering right, which is what makes this a slip rather than a convention. The fix moves the auth block below thewhen, with a comment at the call site saying why the position matters, since nothing in the type system enforces it.This fix is unproven.
GetBlobsRequestSigningTestcovers the same bug class forGetBlobswithout needing a signing key, but it works becausegetBlobsRequesttakes its authenticator as a parameter;ReportingApi.reportcallsauthenticate(owner)inline and has no such seam. Adding one means extracting areportRequest(target, description, authenticate:)builder, which is more than the positional fix this change is scoped to. The first real check is the first live report sent from the Android UI.Why the UI is not here
The Android surfaces are a separate branch. Nothing in them is a dependency of the
flipcash-shared-core-spmpublish, and bundling them would make iOS wait for a UI review it has no stake in.Whoever merges this: the next move is yours. iOS is publish-gated —
FLIPCASH_SHARED_CORE_LOCALexists in this repo'sspm/Package.swiftbut not incode-ios-app, where all three manifests carry an unconditional.upToNextMinor(from: "0.7.0"). Soflipcash-shared-core-spmneeds a0.8.0publish and the tag moved before the iOS work can start.Registrations
A new KMP module is picked up by nothing automatically. Two of the four registrations here fail loudly if forgotten —
include(":libs:reporting")and itskmpUnitTestModulesmembership, without which the aggregate calls atestDebugUnitTestthe module does not have. The other two fail silently:libs/reporting/**in theshared-core-tests.ymlpaths:filter, without which the macOS lane never triggers on a change to the module, and:libs:reporting:iosSimulatorArm64Testin its task list, without which the Kotlin/Native half never runs. Neither omission produces a red check.