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:
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user