The act path the amendment decided — motir-core's half, with the session cookie left host-only
Opened by Zhu Yue ·
Ship motir-core's half of whatever the amendment decided — the routes, the redirect targets and the posture the act affordances on motir.co call into. This card's scope is the amendment's table, read row by row, restricted to rows whose mechanism needs code in this repository.
What is knowably owed regardless of which mechanism the amendment picks (these hold under every option it can reasonably choose, which is why they are here and not on a follow-up):
- The overview WRITE has no public door at all.
publicProjectsService.setPublicOverview(:444) exists, is tested (tests/publicProjects/publicOverviewEdit.test.ts), and was reached only by the deletedoverview-actions.tsServer Action. Whatever the amendment chose — an authenticated route onapp.motir.co, or editing that leaves the public page entirely — the mechanism it names is built here. - A return-to-the-public-page hand-off. Any row whose mechanism is "go to
app.motir.co, act, come back" needs the application to accept a destination and return the visitor to it.proxy.tsalready carries the pattern to follow and the warning that goes with it:CURRENT_PATH_HEADER's doc says an unvalidated redirect target from a request is "the one way this small piece of plumbing could ship a vulnerability." A destination arriving from another origin is validated against an allow-list of the public origin — never reflected. - Two write routes sit OUTSIDE the public contract and outside the cloud gate:
POST /api/public-requests/{id}/upvoteand…/commentsare underapp/api/public-requests/, notapp/api/public/, socontract-coveragedoes not see them,cloud-gate-totalitydoes not gate them, and a self-hosted build answers them. Whatever the amendment made of vote and comment, their placement is disposed of by this card: brought inside the gated, contracted namespace, or left where they are with the reason written in the route's own comment.
The one thing this card may not do, whatever it costs: widen the session cookie. public-surface-hosts.md §4 makes Domain-scoping to .motir.co a rejected alternative, not a deferred one, and the residual exposure the whole arrangement accepts is survivable only on that condition. If the amendment's mechanism appears to require it, the amendment is wrong — stop and say so; do not ship the widening.
Every new or moved route under app/api/public/* owes its contract operation, its cloud gate and its anonymous-posture entry, same as its read siblings.
Acceptance criteria
- Every row of the amendment's affordance table whose mechanism needs
motir-corecode is implemented, and the card's own body lists the rows it discharged and the rows it did not (with the reason: no code needed here, or another card owns it). - The public overview WRITE reaches
setPublicOverviewthrough the mechanism the amendment named, gated so that only a caller the service already authorises as able to manage the project can perform it. - Any hand-off destination accepted from outside is validated against the configured public origin and falls back to a fixed safe destination otherwise; a test drives a hostile value (
https://evil.example,//evil.example,/../) and asserts the fallback. - The placement of
POST /api/public-requests/{id}/upvoteand…/commentsis DECIDED and recorded — either they move underapp/api/public/*with contract operations and the cloud gate, or each route's comment states why it stays outside both. lib/auth.ts's session-cookie configuration is unchanged — the diff touches no cookiedomain, and the motir-core vitest gate asserts it.- Every route this card adds or moves is in
lib/api/public/openapi/operations.tsand passescontract-coverage,contract-drift,cloud-gate-totalityandanonymous-posturewith no new exemption. - No file outside
motir-coreis touched.
Rows discharged — the record this card's first acceptance criterion asks for
⚠️ The amendment landed as AMENDMENT 4, not AMENDMENT 2 (MOTIR-4004 took the ordinal on 2026-09-02; see MOTIR-4108's banner). Its §D table, row by row:
| row | mechanism | discharged here? |
|---|---|---|
| 1 · account menu / sign-in dialog | ABSENT | No motir-core code needed. It becomes a plain link in motir-marketing's chrome — MOTIR-4113 draws it, MOTIR-4115 renders it. |
| 2 · follow | HAND-OFF | Yes — app/act/route.ts. |
| 3 · subscribe | ANONYMOUS-DIRECT | Yes — CORS on /api/public/* in proxy.ts, one allow-listed origin, no Allow-Credentials. |
| 4 · roadmap vote / request upvote | HAND-OFF | Yes — same entry, intent=vote / upvote. |
| 5 · request comment | HAND-OFF | Yes — same entry, intent=comment. |
| 6 · submit a feature request | HAND-OFF | Yes — same entry, intent=request. |
| 7 · in-place overview editing | ABSENT from motir.co | Yes — PATCH /api/projects/{key}/public-overview, an APPLICATION route, deliberately outside the public contract. It gives setPublicOverview the caller it has been without since MOTIR-3951. |
| 8 · viewer-awareness on the reads | always anonymous | Nothing to build. It is a consequence of rows 1–7, not a change: actorUserId is already optional on every read. |
Also discharged: app/api/public-requests/{id}/{upvote,comments} gained the MOTIR_CLOUD gate they never carried, and cloud-gate-totality.test.ts now walks that root, so a future route there cannot escape it. They STAY outside app/api/public/*, with the reason in each route's own comment (§F).
Explicitly NOT here: the visual treatment of a hand-off — the control, the interstitial, the return — is MOTIR-4113's (§I), and the public-side controls that call /act are MOTIR-4119's.
Context refs
motir-core/docs/decisions/public-surface-hosts.md§4 and its AMENDMENT 4 (⚠️ amended from AMENDMENT 2 — MOTIR-4004 took that ordinal) — the constraint and the tablemotir-core/lib/services/publicProjectsService.ts—setPublicOverview(:444),getOverview'scanManagemotir-core/tests/publicProjects/publicOverviewEdit.test.ts— the service test kept for this cardmotir-core/app/api/public-requests/[id]/upvote/route.ts·comments/route.ts—requireCompliantSession(), outside the contract and the gatemotir-core/app/api/public/p/[identifier]/follow/route.ts— the one genuinely session-gated public routemotir-core/proxy.ts—CURRENT_PATH_HEADER's three properties and the open-redirect warningmotir-core/lib/auth.ts— the cookie this card may not widenmotir-core/lib/publicProjects/urls.ts—publicSiteOrigin(), the configured public origin to validate against- the act affordances on motir.co — the consumer of everything here
⚠️ ORDINAL CORRECTED AGAIN, 2026-09-02 — it is AMENDMENT 4. MOTIR-4139 (
b615991c4) merged its own AMENDMENT 3 intopublic-surface-hosts.md— the subprocessor seam — while this story was in flight, so the affordance table is AMENDMENT 4. Every reference to AMENDMENT 2 or AMENDMENT 3 on this card and its siblings means that section. Twice in one day: an ADR ordinal is a serialized resource and nothing in the corpus says so — the amendment's own banner now does.
Discussion
No comments yet.
Adding to this discussion signs you in on app.motir.co and brings you back to this request.