# @leumas/api-kit

The express boilerplate every Leumas router was writing for itself — async wrapping, HTTP-status errors, the admin/ownership predicates, the optional-guard and PassNode-meter seams, pagination and...


The express boilerplate every Leumas router was writing for itself. No domain logic, no express
import — it sits under both halves of the routes/controllers split.

```js
// a router
import { wrap, optionalGuard, meter } from '@leumas/api-kit';

const auth = optionalGuard(requireAuth);
const aiGuard = meter(passnode, 'feature:graphs.ai');

router.post('/:id/statements', wrap(async (req, res) => {
  const { texts, text, source, aiMeta } = req.body || {};
  res.json(await ctl.addStatements({ id: req.params.id, texts, text, source, aiMeta }, { user: req.user }));
}));
```

```js
// a controller — no req, no res, callable from an action or a workflow node
import { httpError, requireOwned } from '@leumas/api-kit';

async function addStatements({ id, texts }, { user }) {
  const doc = await requireOwned((i) => conn.getById('reasoning_graphs', i), { user, id, kind: 'graph' });
  if (doc.kind === 'imported') throw httpError(400, 'imported graphs have no statements');
  ...
}
```

## What each export replaces

| Export | Was |
|---|---|
| `wrap(fn)` | 29 copies of `const wrap = (fn) => …`, in three incompatible bodies |
| `httpError(status, msg, extra?)` / `assertHttp` | ~50 hand-written `Object.assign(new Error(m), { status })`, plus two private copies of this helper |
| `isAdmin(user)` | 8 module-level definitions + 6 inlinings of `req.user?.isAdmin === true` |
| `owns(row, ownerId)` | `own()` in `products/leumas-api/src/app.js` — **unowned rows are shared** |
| `requireOwned(load, { user, id, kind })` | the 6 `ownGraph`/`ownScene`/`ownMap`/`ownStation`/`ownItem`/`ownOutfit` helpers — **unowned rows belong to nobody**; throws instead of writing the response |
| `optionalGuard(g)` | 14 copies of `requireAuth \|\| ((req,res,next) => next())` |
| `meter(passnode, feature)` | 12 copies of the `passnode?.guard ? … : next` block |
| `ownerStore(conn, coll)` / `ownerStores(conn)` | `collStore()` in `app.js` |
| `paginate(query, opts)` | promoted from `router-kit/src/social-lib.js` |

`owns` and `requireOwned` disagree about an unowned row **on purpose** — see the comment at the top
of `src/access.js`. Merging them is a security change, not a cleanup.

## Rules

- Controllers may not import express and may not mention `req`/`res`. Enforced by
  `pnpm check:controllers`.
- The route table and its ordering may not drift. Enforced by `pnpm check:routes` against
  `ops/infra/baselines/api-routes.json`.
- There is no error *class*. `instanceof` across a pnpm workspace is a trap and nothing needs one —
  every consumer reads `.status`, which `errlog.finalHandler()` already does.


---
Source: shared/packages/api-kit/README.md
Canonical: https://docs.leumas.tech/p/packages/api-kit
