Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
# events-routes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Builds the nine-route OpenAPI Hono module mounted by
`createEventsPlugin`. Each route is wrapped by `@hono/zod-openapi`'s
`createRoute` so the package's local Zod schemas drive both
input-validation and generated OpenAPI doc shape. The route layer is
the single point at which auth (`c.get("user")`), authorisation
(`eventRepository.isCreator`), `maxInvitesPerRequest` enforcement,
OTel emission, and EventBus fan-out converge.
The handlers themselves are deliberately thin: every meaningful
side-effect (CREATE row, UPDATE counter, DELETE row, EMIT event)
funnels through the two repositories and the optional `emitEvent`
callback. The callback is the integration point the plugin's
`onReady` uses to forward route events onto the shared
`ctx.eventBus` — that indirection is what fixes the upstream
"local emitter" smell flagged in `enterprise/api-plugin-events.md`.
## Features
- 9 routes: list / get / create / update / delete / rsvp /
cancel-rsvp / my-rsvp / attendees / invite.
- Zod validation on every body / param / query.
- Auth guard on every route via `c.get("user")`.
- Authorisation guard on every mutating route via
`eventRepository.isCreator`.
- `maxInvitesPerRequest` enforcement inside the `/invite` handler.
- OTel counter emission on every successful mutation.
- Typed `emitEvent` callback for cross-plugin fan-out.
## Usage: Basic Example
```ts
import { createEventRoutes } from "@suaveplan/api-plugin-events";
import { OpenAPIHono } from "@hono/zod-openapi";
const routes = createEventRoutes({
eventRepository,
rsvpRepository,
maxInvitesPerRequest: 50,
});
const parent = new OpenAPIHono();
parent.use(async (c, next) => {
c.set("user", await resolveUser(c));
await next();
});
parent.route("/events", routes);
```
## Usage: Advanced Example
```ts
import { createEventRoutes } from "@suaveplan/api-plugin-events";
const routes = createEventRoutes({
eventRepository,
rsvpRepository,
maxInvitesPerRequest: 25,
emitEvent: (event) => {
// Forward onto the host's typed bus
host.eventBus.emit(event.type, event.payload);
// Side-channel: persistent audit log
auditLog.write({
kind: event.type,
payload: event.payload,
at: new Date(),
});
},
});
// `routes` is an OpenAPIHono instance — feed it into the OpenAPI
// document generator:
host.openAPIRegistry.attach(routes);
```
## API Reference
### `createEventRoutes(config: EventRoutesConfig): OpenAPIHono`
Returns an `OpenAPIHono<{ Variables: EventsRoutesVariables }>` with
the nine routes registered.
### `interface EventRoutesConfig`
- `eventRepository: IEventRepository`
- `rsvpRepository: IRsvpRepository`
- `maxInvitesPerRequest: number`
- `emitEvent?: (event: EventsRouteEvent) => void`
### `interface EventsRoutesVariables`
`{ user: { id: string } & Record<string, unknown> | null | undefined }`
— the route handler reads `c.get("user")` from this slot; the host
supplies the auth middleware that populates it.
## Implementation Notes
The handlers manually `try { getUser(c) } catch { return c.json(...) }`
rather than relying on Hono's `onError` because the OpenAPI route
spec requires every documented response status to be reachable from
the handler body. Catching `EventsPluginError(UNAUTHENTICATED)` and
returning a 401 inline keeps the spec honest. The pattern repeats
on every route — verbose, but each block is a single
3-line `try`/`catch`/`json` triple.
The `app.route("/", routes)` line in test fixtures and in the plugin
mount mirrors the parent/child Hono pattern: the parent app owns the
auth middleware, the child app owns the routes. Hono middleware
registered on the parent BEFORE the `.route()` call fires before the
child's handlers, so `c.get("user")` resolves correctly during the
child's `c.req.valid(...)` parsing phase.

View File

@@ -0,0 +1,150 @@
# ics-export
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `ics-export` route module exposes `GET /:id/ics` on top of the
shared events `OpenAPIHono` mount, returning an RFC 5545
`text/calendar` document for a single event. It is the only ICS surface
the plugin ships in 0.1: calendar-feed subscriptions, push delivery,
and recurring-event expansion (RRULE) all remain deferred to follow-up
changes per the proposal's `## Out of scope` list.
The module's contract is narrow on purpose. It owns the auth /
visibility checks for `?with=rsvps`, the failure-counter mapping
(`events.ics.failed_total{reason}`), the success-counter emission, and
the `Content-Disposition: attachment` header. The actual VCALENDAR
construction is delegated wholesale to the pure
`ics-builder/ics-builder` module so byte-level tests can verify fold
and escape rules independently of route plumbing.
## Features
- `GET /:id/ics` — returns `text/calendar; charset=utf-8` with a single
`BEGIN:VCALENDAR` … `END:VCALENDAR` body containing one `VEVENT`.
- `GET /:id/ics?with=rsvps` — same body plus one `ATTENDEE` line per
RSVP row. Requires the requester to be the event creator; non-creators
receive `403`.
- `?with=<unknown>` — surfaces as `422` with
`EVENTS_PLUGIN_CODES.ICS_INVALID_QUERY` via the route module's
`defaultHook`.
- Injectable `Clock` — `DTSTAMP` is sourced from the configured clock
so tests can pin a stable instant; production defaults to `Date.now`.
- Injectable `EventsInstruments` — production binds to the global
bundle via `getEventsInstruments()`; tests pass a fresh bundle to
assert counter emission against a known `MeterProvider`.
- `Content-Disposition: attachment; filename="event-<id>.ics"` on
every success response per RFC 6266 §4 — forces calendar-app download
rather than browser inline render.
## Usage: Basic
```ts
import { OpenAPIHono } from "@hono/zod-openapi";
import {
createIcsExportRoutes,
EventRepository,
RsvpRepository,
} from "@suaveplan/api-plugin-events";
const app = new OpenAPIHono();
app.route(
"/events",
createIcsExportRoutes({
eventRepository: new EventRepository({ db }),
rsvpRepository: new RsvpRepository({ db }),
})
);
// GET /events/event:1/ics → 200, text/calendar
```
## Usage: Advanced
Inject a `Clock` for byte-stable `DTSTAMP` and pre-build a counter
bundle against a known `MeterProvider`:
```ts
import { metrics } from "@opentelemetry/api";
import {
AggregationTemporality,
InMemoryMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
import { OpenAPIHono } from "@hono/zod-openapi";
import {
createIcsExportRoutes,
getEventsInstruments,
resetEventsInstruments,
} from "@suaveplan/api-plugin-events";
const exporter = new InMemoryMetricExporter(
AggregationTemporality.CUMULATIVE
);
const provider = new MeterProvider({
readers: [new PeriodicExportingMetricReader({ exporter })],
});
metrics.setGlobalMeterProvider(provider);
resetEventsInstruments();
const app = new OpenAPIHono();
app.route(
"/events",
createIcsExportRoutes({
eventRepository,
rsvpRepository,
clock: () => Date.parse("2026-05-20T12:00:00Z"),
instruments: getEventsInstruments(),
})
);
```
## API Reference
### `createIcsExportRoutes(config: IcsExportRoutesConfig): OpenAPIHono`
Build a route module registering `GET /:id/ics`. Returns a fresh
`OpenAPIHono` instance the caller mounts on a parent app.
- `config.eventRepository: IEventRepository` — used for `findById` +
`isCreator`.
- `config.rsvpRepository: IRsvpRepository` — used for `findAttendees`
on the `?with=rsvps` variant.
- `config.clock?: Clock` — wall-clock source for `DTSTAMP`. Default:
`() => Date.now()`.
- `config.instruments?: EventsInstruments` — pre-bound counter bundle.
Default: lazy `getEventsInstruments()`.
### Response surface
| Status | Body shape | Counter |
|---|---|---|
| `200` | `text/calendar; charset=utf-8` VCALENDAR document | `events.ics.served_total{with=none\|rsvps}` |
| `401` | `{ error, code: "EVENTS_PLUGIN.UNAUTHENTICATED" }` | `events.ics.failed_total{reason=unauthenticated}` |
| `403` | `{ error, code: "EVENTS_PLUGIN.FORBIDDEN" }` | `events.ics.failed_total{reason=forbidden}` |
| `404` | `{ error, code: "EVENTS_PLUGIN.EVENT_NOT_FOUND" }` | `events.ics.failed_total{reason=not_found}` |
| `422` | `{ error, code: "EVENTS_PLUGIN.ICS_INVALID_QUERY" }` | `events.ics.failed_total{reason=invalid_query}` |
## Implementation Notes
- **Why a separate route module?** The shipped `events-routes` already
carries nine endpoints and a large `Variables` surface. Adding a
tenth route would push the file past the 300-line hard cap and
conflate the JSON-shaped route family with the binary text-stream
ICS endpoint. The split keeps both modules under the file-size
budget and lets the ICS module own its own `defaultHook` without
affecting the other handlers.
- **No `c.json` for the 200 path.** ICS bodies are not JSON. The route
uses `c.body(...)` with an explicit `Content-Type: text/calendar`
header so calendar applications detect the payload correctly.
- **422 is raised by the `defaultHook` on validation failure.** Hono's
default behaviour is `400`; we override to `422` because the request
reached a real handler but carries malformed query parameters —
semantically "unprocessable entity" rather than "malformed request".
- **Why no rate-limit / audit.** Both are scoped out at the plugin
level pending `api-plugin-rate-limit` and `api-plugin-audit`; the
ICS endpoint will inherit composition once those packages land.