feat(19.7-01): streaming routes, MCP mount hook, configurable authorize scopes
- surf.Router.MountStream and recoverStream: a raw, unbuffered route kind
whose http.Flusher reaches the client; panic after the first byte only
aborts the body
- surf.MCPBuildTag and the mountPier hook: mcp.enabled is inert without the
summercms_mcp tag until it is true, then boot fails naming the tag
(pier_off.go is the untagged side)
- wristband parseAuthorizeScopes takes the server's allowed set built from
Options.ScopesSupported; the default set is unchanged
- cooler.KeyPrefix exported for the MCP module's Redis keys
- smoke tests TestStreamingRouteSmoke, TestPierMountSmoke, TestMCPScopesSmoke
- surf, wristband and cooler READMEs, docs/services/{routing,oauth-server}.md
This commit is contained in:
@@ -40,7 +40,9 @@ Use cases:
|
||||
- Opt-in API documentation page: with `http.api_docs.enabled` true, `surf.BuildRouter` mounts `GET /api-docs` (a Swagger UI page) and `GET /api-docs.json` (owned by `summercms.apidocs`, public, no middleware). The spec comes from the one plugin implementing `surf.APIDocsProvider`, as YAML or JSON, converted once at boot to compact JSON in source key order; zero or several providers, or a spec that does not convert, fail boot. The page loads swagger-ui-dist `surf.SwaggerUIVersion` from jsDelivr with Subresource Integrity hashes on all three assets and a `Content-Security-Policy` whose only script source is a per-request nonce; every script tag, the two jsDelivr scripts included, carries it, so no other jsDelivr script can run.
|
||||
- Request log, on by default: every request writes one Info record `http request` from the [diary](../diary/README.md) component `http` after it is served, with `method`, `pattern` (the matched route, such as `GET /posts/{id}`), `status`, `duration_ms` and `request_id` (the `X-Request-Id` header, trimmed and cut to 128 bytes, left out when empty). A request whose client went away adds `client_gone=true`; its 5xx is logged as 499 (client closed request) only when the client had already gone when the status was written and the handler did not panic, so a 5xx written before the cancellation and a recovered panic keep their real status. An unmatched request (a 404 or 405 from the mux, or a CORS preflight answered before routing) has an empty `pattern`. The URL path, the query string, headers and the client address are never logged, for an unmatched request too: a token route requested with another method answers 405 before any route handler runs. Records go to the app's published `*slog.Logger`, else the `http` component logger, whose channels all redact through [sunscreen](../sunscreen/README.md). Silence it with `log.components.http: warn` or turn it off with `http.access_log: false`.
|
||||
- Startup summary and shutdown report for `serve`: once it listens, a block on a terminal (version, environment, URL and admin URL, database host and name, pending migrations, plugins, queue, scheduler, mail, cache, realtime and search drivers; `-v` adds one line per plugin) and, off a terminal, the same facts as a `startup summary` record and a `listening on <addr>` record of component `serve`; after SIGINT or SIGTERM, a report of requests served, drained and aborted, worker jobs, uptime and drain errors. Never a DSN user or password.
|
||||
- A read-only route table (`surf.Router.Routes`), the admin mount status (`surf.Router.AdminPrefix`) and the `serve` and `route:list` commands.
|
||||
- Streaming routes: `surf.Router.MountStream` registers a raw route whose response is not buffered, so the handler's `http.Flusher` reaches the client while it is still running (server-sent events, long-lived streams). Named middleware, `body.limit`, locale, the CORS profile and the access-log pattern apply as on a raw route; a panic before the first byte answers an opaque 500 and after it only cuts the body short. Used by the MCP module; plugin route groups stay buffered.
|
||||
- MCP module hook: with `mcp.enabled` true, `surf.BuildRouter` calls the mount hook the MCP module sets in a binary built with the `surf.MCPBuildTag` tag (`summercms_mcp` in `summer.yaml` `build.tags`), after every plugin route and the admin exist. Off (the default) mounts nothing and changes nothing; on without the tag fails boot with an error naming the tag.
|
||||
- A read-only route table (`surf.Router.Routes`), one route's compiled chain (`surf.Router.Handler`), the admin mount status (`surf.Router.AdminPrefix`) and the `serve` and `route:list` commands.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -127,6 +129,8 @@ The generated application `main` wires surf in with `surf.ServeCommand` and `sur
|
||||
| `surf.Router.RegisterMiddlewareFactory` | Stores a parameterized middleware used as `name:param`. |
|
||||
| `surf.Router.Routes` | Returns a copy of the registered routes as `surf.RouteInfo` values. |
|
||||
| `surf.Router.Handler` | Returns the compiled middleware chain of one registered route by method and pattern, for an in-process dispatch that must run the same guards, throttles, body limit, locale and recovery as an HTTP request. False for an unknown route or before `surf.BuildRouter` compiled the routes. |
|
||||
| `surf.Router.MountStream` | Registers one streaming route (raw, unbuffered, `http.Flusher` passed through) under the bound plugin with the given middleware names; a route another plugin registered for the method and path fails naming it. |
|
||||
| `surf.MCPBuildTag` | `summercms_mcp`: the build tag that links the MCP module; without it `mcp.enabled` true fails boot. |
|
||||
| `surf.Router.AdminPrefix` | The admin mount path and true when `surf.BuildRouter` mounted the cabana admin; false when no plugin registers an admin controller. |
|
||||
| `surf.RouteInfo` | Method, pattern, owning plugin, middleware and raw flag of one route. |
|
||||
| `surf.Use` | Builds a middleware name list for a group. |
|
||||
@@ -173,6 +177,7 @@ The generated application `main` wires surf in with `surf.ServeCommand` and `sur
|
||||
| `http.api_docs.enabled` | `false` | Mounts `GET <http.api_docs.path>` (Swagger UI page) and `GET <http.api_docs.path>.json` (the spec of the one `surf.APIDocsProvider` plugin). Both are public. Environment: `SUMMER_HTTP__API_DOCS__ENABLED`. |
|
||||
| `http.api_docs.path` | `/api-docs` | Page path; the JSON route adds `.json`. Must be an absolute URL path; a plugin route on either path fails boot. |
|
||||
| `http.api_docs.title` | `API documentation` | Page title. |
|
||||
| `mcp.enabled` | `false` | Read here only to call the MCP module's mount hook after every plugin route and the admin are registered. `false` mounts nothing; `true` in a binary built without the `summercms_mcp` tag fails boot with an error naming the tag. The other `mcp.*` keys belong to the MCP module. Environment: `SUMMER_MCP__ENABLED`. |
|
||||
| `http.access_log` | `true` | Logs one `http request` record per request under the `http` component (method, matched pattern, status, duration in milliseconds, request id; never the path or query). Set to `false` to turn it off, or raise `log.components.http` to `warn` to silence it. Environment: `SUMMER_HTTP__ACCESS_LOG`. |
|
||||
| `http.error_pages.enabled` | `true` | Answers browser navigations that hit a framework-generated 404, 405 or 500 with the HTML error page (or a published `surf.ErrorPageRenderer`'s page). `false` restores the plain stdlib and JSON bodies for every request. Environment: `SUMMER_HTTP__ERROR_PAGES__ENABLED`. |
|
||||
| `storage.uploads.serve` | `false` | Mounts the uploads route `GET <storage.uploads.public_path_prefix>/{path...}` (prefix default `/storage/uploads`; it must then be an absolute URL path, or start-up fails) on `attach.StaticHandlerPublic` with `X-Content-Type-Options: nosniff`, a sandboxing CSP and `Content-Disposition: attachment` for anything but a raster image. Prefer a separate origin for uploads in production. Environment: `SUMMER_STORAGE__UPLOADS__SERVE`. |
|
||||
|
||||
Reference in New Issue
Block a user