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 · Work items

MOTIR-1858Done

11.1.2 The shared `/api/v1` route wrapper — bearer-PAT auth + scope gate + `{ code, error }` mapping + request id, proven by `GET /api/v1/me`

Repo: motir-core. One PR. The first /api/v1 code: the wrapper every later endpoint composes, plus the smallest real endpoint that proves it works end to end.

blocked_by 11.1.1 — the wrapper implements that ADR's decisions; building it first would mean deciding them in code.

What to build

1. The wrapper — one helper in lib/api/v1/ that every /api/v1/**/route.ts composes. It:

  • Authenticates the bearer PAT by calling the shipped authenticateApiToken(req, scope) (lib/apiTokens/routeAuth.ts). Do NOT re-implement token parsing, hashing, revocation or expiry checks — that function already returns { ok, userId, workspaceId } or { ok: false, reason: 'unauthenticated' | 'forbidden' }, which is exactly this wrapper's 401/403 split.
  • Maps the result to status: unauthenticated401 (undifferentiated), forbidden403.
  • Builds the ServiceContext the services expect from the resolved { userId, workspaceId }, so the route's single service call runs with the token owner's identity and the product's own access checks apply unchanged.
  • Translates a thrown typed service error into { code, error } + its status; an UNRECOGNISED error propagates as a 500 with no code leaked and no Prisma / stack text in the body.
  • Stamps a request id on the response for support correlation.

2. GET /api/v1/me — the token owner's identity + the workspace the token is bound to + the token's granted scopes. Requires read. Chosen as the proving endpoint because it exercises the whole wrapper with no collection and no resource modelling, so a failure here is unambiguously the wrapper's.

Returning the granted scopes is deliberate and load-bearing: it is how a client discovers what its own token may do without probing endpoints and collecting 403s — the same reason motir doctor exists on the CLI side.

Architecture

Follow the 4-layer contract exactly: the route parses, calls the wrapper, calls ONE service method, returns. No db.*, no $transaction, no business logic in the route. Nothing about this story needs a new service, repository, mapper or migration — if it appears to, stop: that is a card in the owning feature's epic, not here.

Scope BOUNDARY

Ends at the wrapper + /me. It does NOT add pagination (11.1.3), rate limiting (11.1.4) or any resource endpoint (11.2 / 11.3) — but it MUST leave clean seams for the first two, since they compose into the same wrapper rather than wrapping it again. It does NOT modify authenticateApiToken, apiTokensService, lib/mcp/**, or anything under app/api/** outside the new app/api/v1/ tree. It writes no OpenAPI spec (11.4).

Acceptance criteria

  • A valid PAT carrying read gets 200 from GET /api/v1/me with the owner's identity, the bound workspace, and the token's granted scopes.
  • Missing, malformed, unknown, revoked and expired tokens ALL return 401, and the response body does not distinguish which — asserted as five separate cases against one shared expectation, so a future "helpful" error message cannot silently turn the endpoint into a token oracle.
  • A valid token WITHOUT the required scope returns 403 — distinct from 401, and never a 200 with an empty body.
  • A typed service error becomes { code, error } with its mapped status; an unexpected error becomes a 500 whose body contains no Prisma message, no stack and no code — asserted by throwing a raw error through the wrapper.
  • Every response carries the request id header.
  • The route contains no db.* call and no $transaction — the 4-layer contract, asserted by the architecture guard, not just by review.
  • The wrapper composes: a second route can adopt it without copying auth or error-mapping logic (demonstrated by the tests exercising a fixture route, not only /me).
  • Unit + integration tests ship in the same PR, against real Postgres (the repo convention — no mocked DB), covering every branch above.
  • The per-file coverage floor (≥90% branch/fn/line) holds on every new file.

Context refs

  • lib/apiTokens/routeAuth.tsauthenticateApiToken(req, requiredScope) and its ApiTokenAuthResult discriminated union; the 401/403 split to map, NOT to re-implement.
  • lib/mcp/scopes.tsTokenScope, the scope type the wrapper takes.
  • lib/mcp/auth.tsverifyMcpToken, the MCP-side sibling; read it for the non-disclosure behaviour this must match.
  • lib/mcp/context.ts + lib/workItems/serviceContext.ts — how a resolved token becomes the ServiceContext services expect.
  • app/api/work-items/[id]/route.ts — the shipped { code, error } + status envelope to mirror.
  • app/api/work-items/[id]/acceptance-evidence/upload-token/route.ts — the existing REST route that already authenticates a PAT; the closest working example.
  • Decision: 11.1.1. Parent story: 11.1.