Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Roadmap

MOTIR-4042Done

A self-hosted build still publishes an OpenAPI contract for a public surface it does not serve — /api/openapi/public.json is outside the gate

Opened by Zhu Yue ·

The defect

MOTIR-3908 makes the public-projects capability ABSENT off-cloud: every route under app/api/public/ answers 404 { code: 'NOT_FOUND' } when MOTIR_CLOUD is unset.

GET /api/openapi/public.json is not under that path and was not gated. On a self-hosted build it still serves the full published contract — twelve operations, their parameters, their response schemas — for twelve routes that all answer 404. A reader (or a generated client) is handed a document describing a surface the instance does not have.

It is the shape the story explicitly forbids one surface over: "A self-hosted build does not ship a broken route or a stack trace; it ships a deliberate answer."

Why it was not simply gated in MOTIR-4034

Reproduced on origin/main 4f3121228: app/api/openapi/public.json/route.ts declares

export const dynamic = 'force-static';

A force-static route is evaluated at BUILD time, so an isCloud() call inside the handler reads the flag of the machine that built the image, not of the deployment running it. A self-hoster who later sets MOTIR_CLOUD=true would keep the build-time answer. That makes the gate a real decision with a cost — drop force-static (the route becomes dynamic, losing a cache the comment justifies as "it changes only when the code does"), or gate at a layer that runs per-request — rather than the two-line change the sibling routes took. Out of scope for that card, and its own decision here.

The three candidate answers, none obviously right

  1. Drop force-static and gate in the handler. Simplest; costs the static cache on a document that is genuinely static per deploy.
  2. Gate in the proxy / middleware, which runs per request whatever the route's rendering mode. Keeps the cache; puts one more path in the matcher.
  3. Leave it served, and say so. Defensible — the document describes the PRODUCT, and /docs is deliberately not gated for the same reason. If this is the answer it needs to be written down, because right now it is not a decision, it is an omission.

Whichever is chosen, tests/api/public/cloud-gate-totality.test.ts should reach it: today that guard enumerates app/api/public/** and this route is outside the tree it walks, which is exactly why the gap survived a totality check.

Acceptance criteria

  • The disposition of GET /api/openapi/public.json on a build with MOTIR_CLOUD unset is DECIDED and recorded in the route's own header, with the reason.
  • If it is gated, both arms are tested and the force-static consequence is stated: the answer must follow the DEPLOYMENT's flag, not the builder's.
  • If it is deliberately left served, the reasoning is in the route header and cloud-gate-totality.test.ts carries an assertion recording that as intent rather than as an oversight.
  • Nothing under app/api/public/ changes behaviour in either arm.

Context refs

  • motir-core/app/api/openapi/public.json/route.ts — the route and its force-static declaration
  • motir-core/lib/publicProjects/cloudGate.ts — the gate the sibling routes use
  • motir-core/tests/api/public/cloud-gate-totality.test.ts — the totality guard, and the tree it walks
  • motir-core/docs/decisions/public-surface-hosts.md — the arrangement this serves

Discussion

No comments yet.

Adding to this discussion signs you in on app.motir.co and brings you back to this request.

Add a comment