Files
2026-05-27 23:29:53 +01:00

108 lines
5.9 KiB
Markdown

# @suaveplan/api-graphql-ui-graphiql — GraphiQL UI mount for the Suaveplan API plugin system. Exposes createGraphiqlUi() — a factory returning a RouterPlugin that registers GET /graphql/ui (or any configured endpoint) and serves a self-contained HTML shell + bundled assets with a strict CSP. Default GraphQL explorer when operators want GraphiQL; sibling api-graphql-ui-altair covers the Altair alternative.
GraphiQL UI mount for the Suaveplan API plugin system. Stage 7 of
the umbrella plan.
`createGraphiqlUi(opts)` returns a `RouterPlugin` that registers
`GET /graphql/ui` (or any configured endpoint) on a host
`@suaveplan/api-router` `Router`. The endpoint serves a self-contained
HTML shell plus bundled JS/CSS assets — no CDN fetches.
<!-- begin:auto-docs -->
## Documentation tree
- [assets](./src/assets/assets.md) — Bundled static-asset registry for the GraphiQL UI plugin. The HTML shell references two assets (`graphiql.js`, `graphiq…
- [assets](./src/assets/assets.md) — Bundled static-asset registry for the GraphiQL UI plugin. The HTML shell references two assets (`graphiql.js`, `graphiq…
- [error-codes](./src/error-codes/error-codes.md) — Owns the stable `GRAPHQL_UI_GRAPHIQL.*` error-code registry consumed by the GraphiQL UI plugin. Every code in the regis…
- [error-codes](./src/error-codes/error-codes.md) — Owns the stable `GRAPHQL_UI_GRAPHIQL.*` error-code registry consumed by the GraphiQL UI plugin. Every code in the regis…
- [factory](./src/factory/create-graphiql-ui.md) — Public entry point of the package. `createGraphiqlUi(opts)` returns a `RouterPlugin` — a plain value with `name`, `vers…
- [create-graphiql-ui](./src/factory/create-graphiql-ui.md) — Public entry point of the package. `createGraphiqlUi(opts)` returns a `RouterPlugin` — a plain value with `name`, `vers…
- [headers](./src/headers/csp.md) — Owns the `Content-Security-Policy` header values emitted on the GraphiQL HTML response. The policy is hard-coded to a p…
- [csp](./src/headers/csp.md) — Owns the `Content-Security-Policy` header values emitted on the GraphiQL HTML response. The policy is hard-coded to a p…
- [html](./src/html/render-shell.md) — Renders the GraphiQL HTML shell. The output embeds the configured GraphQL endpoint, subscription endpoint, theme, and e…
- [render-shell](./src/html/render-shell.md) — Renders the GraphiQL HTML shell. The output embeds the configured GraphQL endpoint, subscription endpoint, theme, and e…
- [instruments](./src/instruments/instruments.md) — Owns the OpenTelemetry instrument bundle emitted by the GraphiQL UI plugin. A single `Counter` (`graphql.ui.requests_to…
- [instruments](./src/instruments/instruments.md) — Owns the OpenTelemetry instrument bundle emitted by the GraphiQL UI plugin. A single `Counter` (`graphql.ui.requests_to…
- [options](./src/options/options.md) — Zod-validated configuration surface for `createGraphiqlUi`. Owns `GraphiqlUiOptions` (resolved shape with every default…
- [options](./src/options/options.md) — Zod-validated configuration surface for `createGraphiqlUi`. Owns `GraphiqlUiOptions` (resolved shape with every default…
<!-- end:auto-docs -->
## Why opt-in
Per the design doc (`docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`
§10.6) the API foundation reserves `GET /graphql/ui` for an interactive
explorer but does not bundle one by default — UI choice is operator-
chosen so the runtime image stays minimal for production deployments
that don't need it.
Operators wire ONE of:
- `@suaveplan/api-graphql-ui-graphiql` — canonical GraphiQL (this
package).
- `@suaveplan/api-graphql-ui-altair` — Altair alternative.
The GraphQL engine adapters (`api-graphql-yoga`, `api-graphql-apollo`)
return 404 at `/graphql/ui` when no UI plugin is wired.
## Quick start
```ts
import { Router } from "@suaveplan/api-router";
import { createGraphiqlUi } from "@suaveplan/api-graphql-ui-graphiql";
const router = new Router({ plugin: "host" });
createGraphiqlUi({}).register(router);
// GET /graphql/ui -> the explorer
// GET /graphql/ui/assets/graphiql.<hash>.js -> bundled JS
// GET /graphql/ui/assets/graphiql.<hash>.css -> bundled CSS
```
## Configuration
| Option | Default | Notes |
|---|---|---|
| `uiEndpoint` | `/graphql/ui` | Must start with `/`. |
| `graphqlEndpoint` | `/graphql` | Embedded into the explorer config. |
| `subscriptionEndpoint` | `/graphql/ws` | Embedded into the explorer config. |
| `theme` | `"system"` | `"light"` / `"dark"` / `"system"`. |
| `defaultQuery` | `""` | HTML-escaped before embedding. |
| `defaultVariableEditorOpen` | `false` | Opens the variables pane on load. |
| `editorOptions` | `{}` | CodeMirror passthroughs. |
| `meterProvider` | OTel global | OTel `MeterProvider` for the counter. |
## Security posture
- **Bundled assets only.** Every `src` / `href` in the rendered HTML
points inside the package's own `/<uiEndpoint>/assets/` namespace.
Cross-checked by `src/html/render-shell.test.ts`.
- **Strict CSP.** `default-src 'self'; style-src 'self' 'unsafe-inline';
script-src 'self'; img-src 'self' data:; connect-src 'self'`.
Inline scripts are NOT permitted. Cross-checked by
`src/headers/csp.test.ts`.
- **Cache-immutable assets.** Bundled JS/CSS carries
`Cache-Control: public, max-age=31536000, immutable`.
- **Stale-hash tolerance.** Hashed filenames resolve any matching
stem+ext to the current asset bytes — rolling deploys don't
404 in-flight sessions.
## OTel
- `graphql.ui.requests_total { ui: "graphiql" }` — counter incremented
on each successful HTML response. Asset hits are NOT counted.
## Error codes
- `GRAPHQL_UI_GRAPHIQL.ASSET_MISSING` — asset lookup failure.
- `GRAPHQL_UI_GRAPHIQL.ENDPOINT_COLLISION` — `<uiEndpoint>` already
registered by another plugin.
## Tests
```bash
bunx --bun turbo lint typecheck test:coverage --filter=@suaveplan/api-graphql-ui-graphiql
```
100% statements / branches / functions / lines required on touched
modules (`provider: istanbul`).