11.1.7 `/api/v1/me` and `/api/v1/workspaces` MAP through `lib/api/v1/identity/schema.ts` — the shape becomes the value the routes emit, not a description beside them
Repo: moooon-B-V/motir-core · ONE PR. The code half of MOTIR-2195's decision, carved out because it changes response-shaping code and is not a documentation edit (that card's own third acceptance criterion says so).
What was decided, and what is left
ADR Amendment 5 (docs/decisions/public-api-conventions.md, 2026-08-05, shipped by MOTIR-2195) settled two things. The first — Story 11.1 owns the identity and workspace shapes, in lib/api/v1/identity/ — is a record change and landed with the amendment. The second is this card:
A v1 route MAPS THROUGH its schema — the shape is the value the route emits, not a description written beside it.
Every other v1 resource already does it: app/api/v1/projects/route.ts hands presentProject to the pager, the work-item routes return presentWorkItemDetail / presentWorkItemSummary, the ready set maps through presentReadyItem. The two OLDEST endpoints in the API do not. meSchema and workspaceSummarySchema describe what app/api/v1/me/route.ts and app/api/v1/workspaces/route.ts return; the routes build their rows from inline object literals that no schema constrains.
The gap that closes. A field added to /me's inline literal does not fail typecheck — it fails only tests/api/v1/openapi-operations-coverage.test.ts's .strict() parse, at run time, in a suite about something else. Every other resource fails at the compiler, at the seam that decides what the public API says. That is the weaker guarantee this card removes: the document and the endpoint become the SAME expression rather than two a test says agree today.
What must NOT change: the reason the routes shape explicitly. app/api/v1/me/route.ts records it — "the response is shaped explicitly rather than spread, because verify returns the raw Prisma User row and a public API must never leak one" — and workspaces/route.ts says the same for its rows. The mapper does not weaken that instinct, it institutionalises it: field by field, never spread, in ONE place instead of in each handler. ADR Amendment 2's corollary is the same rule one level up.
Response bytes do not change. This is a refactor of WHERE the shape is expressed, not WHAT it is. /me keeps { user: { id, name, email }, workspaceId, scopes }; a workspace row keeps { id, name, slug, createdAt } with createdAt an ISO string. Any byte difference is a §8 breaking change and a defect in this card.
Acceptance criteria
lib/api/v1/identity/schema.tsexportspresentMeandpresentWorkspaceSummarymappers on the pattern ofpresentProject(lib/api/v1/projects/schema.ts:101): each shapes field by field and never spreads, takes the SERVICE-side value as its input type, and returns thez.infertype of its schema.app/api/v1/me/route.tsreturnspresentMe(...)'s output andapp/api/v1/workspaces/route.tspassespresentWorkspaceSummaryaspaginateKeyset's row mapper — neither handler contains an inline object literal for a response row any more, found by reading both files rather than by running until green.- A field added to the underlying Prisma row (
User,Workspace) still cannot reach either response — the mapper's input type is the service's return value, so widening the row changes nothing until the mapper is edited. - The response BYTES are unchanged for both endpoints: a test asserts each endpoint's exact key set (
/me:user.{id,name,email},workspaceId,scopes; a workspace row:id,name,slug,createdAtas an ISO string) against a REAL response driven with a real PAT, not against a fixture built from the schema. tests/api/v1/openapi-operations-coverage.test.tsandtests/api/v1/openapi-drift-guard.test.tsstill pass — their assertions are preserved; neither is edited to accommodate the change.- The WHY comment moves with the shaping: the "must never leak a raw Prisma row" reasoning lives in the schema module beside the field list, and each route keeps a one-line pointer to it rather than a copy.
lib/api/v1/identity/schema.ts's header note — currently "These schemas DESCRIBE today; they will EMIT" — is updated to state that they now emit, and to drop the "until it lands" clause.- The per-file coverage floor (≥90%) holds on every file this card changes.
Context refs
lib/api/v1/identity/schema.ts—meSchema,workspaceSummarySchema, and the header this card finishes.app/api/v1/me/route.ts·app/api/v1/workspaces/route.ts— the two inline literals, and the comments that explain why they are explicit.lib/api/v1/projects/schema.ts:101(presentProject) +app/api/v1/projects/route.ts— the owned-and-mapped-through pattern to mirror, including how a mapper is handed to a pager.lib/api/v1/workItems/schema.ts—presentWorkItemSummary/presentWorkItemDetail, the same pattern at a larger scale.docs/decisions/public-api-conventions.md— Amendment 5 §4 (this decision) and Amendment 2's corollary (a v1 response is a schema's output).tests/api/v1/openapi-operations-coverage.test.ts— drives both routes with a real PAT and parses what they actually return; the existing proof the shapes match.