Skip to content

feat(sdk,auth): a web host — @HttpRoute pages and SSE routes through a hookable http:route flow, with loopback-token sign-in - #1020

Merged
alexmercerpo merged 14 commits into
feat/m20-shared-server-attachfrom
feat/m16-web-host
Nov 3, 2026
Merged

alexmercerpo merged 14 commits into
feat/m20-shared-server-attachfrom
feat/m16-web-host

Conversation

@frontegg-david

@frontegg-david frontegg-david commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #1019

What and why

Proposal item 2.6: a web host for people, on the same container.

The reported problem: a server that showed people a live view of what its agent worked on (a board, a queue of slow
queries) ran a second, hand-written HTTP server for it, with its own token login, host guard, same-origin check and SSE
hub, in its own process, rebuilding every piece of state the MCP server already held. http.routes mounts handlers on
the MCP listener, but they get no DI, no people login, no same-origin check and no SSE.

What this PR adds:

  • @HttpRoute({ method, path, auth?, sse? }) classes, listed in @App({ routes }). HttpRouteContext gives
    this.get() / this.tryGet() over the app's providers (the same instances its tools get), this.request,
    this.params, this.person, this.scope, this.fetch(). handle() answers with an httpRespond output as it is,
    an object or array as JSON, a string as text, nothing as 204; a thrown PublicMcpError keeps its status, anything
    else is a 500 without its message. path is Express-style, or a function called once at boot (a path from options,
    which the dashboard needs for basePath). Metadata is Zod-checked when the class is decorated
    (InvalidDecoratorMetadataError); at boot a route on an MCP path (ReservedRouteCollisionError), a duplicate method
    and path, a class that is not an @HttpRoute, and a people route without people auth fail (InvalidHttpRouteError).
  • A hookable http:route flow (HttpRouteHook): checkIpFilter → findRoute → acquireQuota → checkSameOrigin
    → authenticatePerson → handle. Adapters only translate: the Express host mounts the flow's middleware in each
    scope ahead of http:request, and the fetch handler lets a scope's route claim its request before the MCP entry path
    and across scopes (a standalone app's route outside its base path included), trying the scopes in the order the
    Express host mounts them, so a path that routes of two scopes serve goes to the same route on both. Both run the same
    flow; http.routes and serveHttpRouteWeb are unchanged.
  • A GET route on the MCP path (/ by default) serves page loads there; a GET that asks for text/event-stream
    or carries Mcp-Session-Id still reaches the MCP endpoint, whose plain-GET answer was 405. Every other method
    there, and the other reserved paths, stay refused, and so does an SSE route there, which an EventSource (always
    asking for text/event-stream) could never reach.
  • SSE routes from M13's topic API. sse: { store, topics, coalesceMs = 50, heartbeatMs = 15000 }: handle() makes
    the view, sent at once and again after each burst of publishes on the topics (a resourceStore() key publishes its
    name), with a : keep-alive comment when idle. Only the latest view waits for a reader that falls behind: the Node
    writer now takes the next chunk of an sse output only once a full socket drains (it wrote every chunk at once, so a
    reader that stopped reading made the server buffer each view), and the fetch handler's stream is pull-based. The
    stream ends, and unsubscribes, when the client disconnects (the Node writer ends an sse output's iterator when the
    response closes, or at once when the client left before the response was written; the fetch handler's stream already
    cancels it), on a people route when the person's session expires or ends (checked before each view and heartbeat),
    and when the server shuts down (Scope.shutdown() closes the streams before the host's drain, and SSE responses carry
    Connection: close so a client's keep-alive socket does not hold the drain).
  • People auth, in libs/auth (LoopbackTokenAuth, peopleAuthOptionsSchema(), peopleSessionCookie()), turned on
    with http: { auth: { people: 'loopback-token' } } (or an object with codeTtlMs, sessionTtlMs,
    maxFailedAttempts, lockoutMs, cookiePath):
    • a token is printed once to stderr at startup ([frontmcp] people sign-in token: …); takeToken() hands it out
      once, and nothing logs it;
    • POST /_frontmcp/people/code with Authorization: Bearer <token> returns a one-time code (single use, 60 s TTL by
      default, at most 10 min); a tool gets one without the token from this.get(LoopbackTokenAuth).createCode();
    • GET /_frontmcp/people/login?code=… serves a page whose form posts the code back, and signs nothing in itself;
    • POST /_frontmcp/people/login (same-origin only) spends the code and sets
      frontmcp_people=<random>; Path=<cookiePath>; Max-Age=…; HttpOnly; SameSite=Strict (+ Secure over https); the
      cookie value is a fresh random value, never the token or a code;
    • the token, codes and sessions are kept as SHA-256 digests and compared with timingSafeEqual from
      @frontmcp/utils; maxFailedAttempts (5) bad tokens or codes from a client lock it out for lockoutMs (60 s), a
      good code included;
    • one people auth per server: its scopes share the token and the sessions. With it set, routes need a signed-in person
      unless they say auth: 'public'.
  • A same-origin check on POST/PUT/PATCH/DELETE (isSameOriginRequest, next to host-validation), with bug
    PR 05's Origin rules: an Origin is accepted when its host is the request's Host (or X-Forwarded-Host behind a
    proxy trusted with FRONTMCP_TRUST_PROXY), another loopback spelling of the same port, or an explicit
    http.cors.origin / dnsRebindingProtection.allowedOrigins origin (origin: true or '*' lists none); without an
    Origin, Sec-Fetch-Site must be same-origin or none when present; with neither header (a CLI, an agent) the
    request is allowed. On a loopback Node server PR 05's host validation refuses a cross-origin request before any flow;
    this check also covers the fetch handler, routable binds and permissive CORS.
  • plugin-dashboard moves onto it. The page is DashboardPageRoute, an auth: 'public' route at basePath
    guarded by the dashboard's own token as before; the dashboard:middleware provider is gone. The page now runs through
    the flow: behind throttle.ipFilter (the old middleware skipped it) and on createFetchHandler() too (it answered
    404 there). Public options are unchanged.
  • Typed refusals: PersonNotSignedInError (401, PERSON_NOT_SIGNED_IN), CrossOriginRequestError (403,
    CROSS_ORIGIN_REQUEST), SignInRefusedError (401, SIGN_IN_REFUSED), SignInLockedError (429, SIGN_IN_LOCKED);
    boot errors InvalidHttpRouteError (INVALID_HTTP_ROUTE) and InvalidPeopleAuthOptionsError
    (INVALID_PEOPLE_AUTH_OPTIONS).

Threat model. People auth is for a person at the machine the server runs on; the attacker is a page on another site
in that person's browser, or a client on the network.

  • Defended: a cross-site mutation, the sign-in post included (the same-origin check, backed by Fetch Metadata, and the
    SameSite=Strict cookie); guessing the token, a code or a session (32 random bytes each, kept as SHA-256 digests,
    compared in constant time, plus the lockout); a code spent twice, also by two requests at once (the redemption is
    synchronous); session fixation (the cookie value is minted at sign-in); an open redirect through next (a path on
    this server only); a lockout dodged by rotating X-Forwarded-For (it is read only behind a trusted proxy); a slow SSE
    reader growing the server's memory (one pending view, and the Node writer waits for the socket); a client that leaves,
    even before the stream opens, keeping a subscription; a stream outliving its session.
  • Relies on host validation: the same-origin check compares Origin with the request's own Host, which a page on a
    DNS-rebound name sets to that name. dnsRebindingProtection refuses such a host: by default on a loopback Node
    server, and on createFetchHandler() once allowedHosts is set. A rebound page never carries the host-only session
    cookie, so people routes stay closed either way; public routes are as open as they are to any client.
  • Known limits: the one-time code travels in the sign-in link's query (it is single use, lives 60 s, is not spent by
    the GET, and the page sends Referrer-Policy: no-referrer); cookies are not isolated by port, so another server on
    localhost receives frontmcp_people, and signing in to one FrontMCP server on localhost signs out of another
    (cookiePath separates them); the lockout is keyed by the peer address, so clients that share one (behind a proxy
    that is not trusted, or a fetch handler that is given none) share a lockout, and any of them can hold sign-in shut for
    lockoutMs; there is no sign-out route yet (LoopbackTokenAuth.endSession() ends a session, and its open streams);
    sessions are in memory.

Deviations from the plan, with the reason:

  • Routes are declared in @App({ routes }), not in http.routes. http.routes is server metadata that every scope
    copies (a standalone app's scope included), so classes there would be mounted once per scope; an app gives a route the
    providers its tools use, which is what "DI shared with tools" asks for. http.routes keeps its plain handlers.
  • auth: { people: 'loopback-token' } lives under http (http.auth.people): it guards the web host, not the MCP
    endpoint, whose auth is a union of modes.
  • http.auth.people is checked when the server builds its routes rather than in the eager http options schema, so a
    browser bundle that does not use people auth does not carry its schema.

User-facing change

  • What changed: new @HttpRoute, HttpRouteContext, HttpRouteHook, the http:route flow, @App({ routes }),
    http.auth.people, LoopbackTokenAuth (exported from @frontmcp/sdk and @frontmcp/auth), the sign-in routes under
    /_frontmcp/people/, and the error classes above. The dashboard page is an @HttpRoute.
  • Before → after:
    • Dashboard page on createFetchHandler(): 404 → served.
    • Dashboard page and throttle.ipFilter: not checked → checked (403 for a denied address).
    • Dashboard page with the dashboard turned off: passed on to the dashboard's MCP endpoint → 404
      {"error":"The FrontMCP dashboard is disabled."}.
    • An sse output on the Node host: its iterator ran on after the client left, and every chunk was written at once →
      it is ended (return()) when the response closes, or at once when the client left before the response was
      written, and the next chunk is taken only once a full socket drains. MCP 2026-07-28 streams use it too.
    • New reserved names: a user route on /_frontmcp/people/* collides with the sign-in routes when people auth is on.
    • An SSE @HttpRoute on the MCP path fails startup with InvalidHttpRouteError (an EventSource there reaches the
      MCP endpoint).
  • frontmcp.dev pages affected (if known): a new HTTP routes page (/reference/sdk/http-route); the @App reference
    (routes); the HTTP configuration page (http.auth.people); the hooks page (HttpRouteHook, the http:route
    stages); the dashboard plugin page.
  • Skills catalog entries updated: frontmcp-development/references/create-http-route.md (new, with its manifest
    row), frontmcp-development/SKILL.md, frontmcp-development/references/decorators-guide.md,
    frontmcp-development/references/official-plugins.md (dashboard), frontmcp-config/references/configure-http.md. The
    catalog check that refuses auth: 'public' shorthand now skips @HttpRoute( lines, where auth is the route's own
    option.

Browser bundle: +608 bytes minified on both measured bundles (174 of them are the Node writer's drain wait, which the browser build carries); see "Bundle size" below for the numbers and the budget change. The http-route/ modules are server-only and checked as such (http routes in the
feature-module check).

Tests

  • Unit
    • libs/auth/src/people/__tests__/loopback-token.auth.spec.ts (30): options and their refusals, the token handed out
      once, single-use codes, code TTL, constant-time comparison, lockout per client and its end, session TTL, sign-out,
      the cookie never being the token, the cookie attributes.
    • libs/sdk/src/server/security/__tests__/same-origin.spec.ts (22): own origin, a trusted proxy's forwarded host, a
      forwarded host the page set itself (also one a trusted proxy passed on, from a browser sending Fetch Metadata),
      loopback aliases (IPv6 included), listed origins, null, a sibling subdomain, an http page of an https host,
      Sec-Fetch-Site values, CLI requests.
    • libs/sdk/src/server/__tests__/write-http-response.sse.spec.ts (3): the Node writer waits for a full socket to
      drain, ends the stream when the client left before the response was written, and when it leaves while the socket is
      full.
    • libs/sdk/src/http-route/__tests__/sse-route.stream.spec.ts (13): first view, coalescing a burst, a publish during
      a view, latest view for a slow reader, undefined views, failing views, heartbeat, close and unsubscribe, ending
      once the reader is no longer authorized (at the next view and at the next heartbeat).
    • libs/sdk/src/http-route/__tests__/http-route.flow.spec.ts (56): the same scenarios on the Express host and the
      fetch handler: people auth, the sign-in pages, cookie attributes (Secure over https), a code spent by two requests
      at once, cross-site refusals (a spoofed X-Forwarded-Host included), response kinds, a route on the MCP path, SSE
      updates and unsubscribe on disconnect, a signed-in stream ended with its session, SSE closed on shutdown, DI shared
      with a tool, a tool handing out a code, lockout (429, also with a new X-Forwarded-For each time), a plugin hook
      on the flow, the token printed once for several scopes, throttle.ipFilter, a path that routes of two splitByApp
      scopes serve going to the first on both adapters, and a client that leaves before its stream opens (held by a hook
      on handle) leaving no subscription on the Express host.
    • libs/sdk/src/http-route/__tests__/http-route.validation.spec.ts (22): decorator refusals and boot refusals (an SSE
      route on the MCP path included).
    • plugins/plugin-dashboard/src/__tests__/dashboard.page-route.spec.ts (8): the page through the fetch handler,
      basePath, disabled, token gate and cookies, Secure, throttle.ipFilter; the old middleware tests are replaced.
    • libs/sdk/src/hooks/__tests__/fixtures/typed-hook-ctx.fixture.ts: HttpRouteHook added to the typed hook check.
  • E2E
    • demo-e2e-web-host (new, 7/7): the token printed once, sign-in with a one-time code posted from the page, a reused
      code refused, pages refused without a session, a code posted from another site refused, a cross-site mutation
      refused, an SSE page hearing the change a tool made, a page and a tool sharing a provider.
    • demo-e2e-dashboard 12/12, demo-e2e-dts-consumer 26/26 (new http-routes-must-compile.ts),
      demo-e2e-bundle-size 12/12, demo-e2e-protocol-20260728 147/147, demo-e2e-notifications 50/50 (the writer
      change), demo-e2e-security 28/28, demo-e2e-standalone 54/54 (legacy SSE included), demo-e2e-cimd 11/11,
      demo-e2e-transport-recreation 40/40.
  • Ran: jest for sdk (622 suites, 8,705 tests), auth (100 suites, 2,841 tests), plugin-dashboard (13 suites,
    223 tests), nx test skills;
    nx run-many -t lint and -t build for auth, sdk, plugin-dashboard, skills; type checks of the new spec
    files with tsc --noEmit; node scripts/fix-unused-imports.mjs; prettier on every changed file.

Bundle size

@HttpRoute, its option schema and the http:route flow stub reach the browser bundle; SSE, people auth and the route runtime stay out (checked as server-only in demo-e2e-bundle-size). Measured against #1019's head (2e390cc, after the review fixes below it): create() + one tool 1,332,315 → 1,332,923 bytes minified (+608; +242 gzipped), create() + WebMcpPlugin + one tool 1,339,494 → 1,340,102 (+608; +255 gzipped). The two browser budgets move up from #1019's by exactly that, to 1,333,877 and 1,340,744 (24acbf1).

Summary by CodeRabbit

  • New Features
    • Added HTTP routes for pages, APIs, and live updates, with optional people sign-in and session-based access.
    • Added same-origin protections for route mutations and improved handling of live event streams.
    • The dashboard page is now served as a route, with authentication, cookie, and access-control behavior preserved.
  • Bug Fixes
    • Improved stream handling when clients disconnect or responses encounter backpressure.
  • Documentation
    • Added guidance for configuring and building HTTP routes, authentication, and live updates.
  • Tests
    • Expanded coverage for route behavior, sign-in, security, streaming, and TypeScript declarations.

…kable http:route flow, with people auth and SSE from resource topics

@HttpRoute classes listed in @app({ routes }) run through a new http:route flow on
the Express host and the fetch handler alike: the IP filter, the global rate limit,
a same-origin check on POST, PUT, PATCH and DELETE, people auth, then handle()
with the app's providers, the ones its tools get. An SSE route sends handle()'s
view at once and after each burst of publishes on its resource-store topics, with
a heartbeat, and ends when the client leaves or the server shuts down.

http.auth.people: 'loopback-token' (libs/auth) prints a token once at startup; the
token buys single-use sign-in codes with a short TTL, and a code posted from the
server's own page buys an HttpOnly, SameSite=Strict, Path-scoped session cookie.
Tokens, codes and sessions are compared in constant time, and bad ones lock a
client out.

The dashboard page is now DashboardPageRoute, so it is served on every adapter and
behind throttle.ipFilter, with its options unchanged.
…undle, check http.auth.people at boot, and type-check @HttpRoute in the dts consumer

The @HttpRoute metadata schema now lives with the decorator, and the people auth
schema is built on first use, so a browser bundle that declares neither carries
neither. http.auth.people is parsed when the server builds its routes and fails
with InvalidPeopleAuthOptionsError. The bundle check names the http-route
modules as server-only.
…e dashboard page route and @HttpRoute in the decorators guide

Also: build the route table from the scope in the server-only helper, check that
a listed route is an @HttpRoute class at boot, and test a hook on the http:route
flow, a tool that hands out sign-in codes, and the sign-in lockout over HTTP.
…ture, type the dashboard page spec, and export the route flow's state schema
…s response, and test the Secure session cookie over https
…-origin check only behind a trusted proxy

A page can send X-Forwarded-Host itself once permissive CORS lets its preflight
through, so naming its own host there passed a cross-site mutation. The route's
same-origin check now reads it only with FRONTMCP_TRUST_PROXY, as the issuer and
discovery URLs do. Also: the dashboard README lists the page route.
…lready left

The Node writer wrote every chunk of an sse output at once, so a reader that stopped
reading made the server buffer each view, and a client that left before the response
was written was never noticed: its stream, subscription and heartbeat ran on. It now
takes the next chunk only once the socket drains, and ends the iterator at once when
the response is already destroyed.
…aims the same on both adapters

- An SSE stream on a people route asks for the session before each view and heartbeat,
  and ends once it expired or was ended; it used to run on after sign-out.
- The fetch handler tries the scopes' routes in the order the Express host mounts them,
  so a path that routes of two scopes serve goes to the same route on both adapters.
- An SSE route on the MCP path fails startup: an EventSource always asks for
  text/event-stream, which the MCP endpoint serves there.
- Tests for a code spent by two requests at once, a lockout that a new X-Forwarded-For
  does not dodge, a client that leaves before its stream opens, and more Origin cases.
…nto feat/m16-web-host

# Conflicts:
#	apps/e2e/demo-e2e-dts-consumer/e2e/dts-consumer.e2e.spec.ts
#	libs/auth/src/index.ts
#	libs/sdk/src/errors/index.ts
…1019

@HttpRoute, its option schema and the http:route flow stub reach the browser bundle (SSE, people auth and the route runtime stay out): both measured bundles grow by 608 bytes minified over #1019's head. The two budgets move up by exactly that.
@frontegg-david
frontegg-david marked this pull request as ready for review November 2, 2026 20:39
@frontegg-david
frontegg-david added this pull request to stack #997 November 2, 2026 20:39
@frontegg-david

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

Next included review available in 28 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 24acbf1 and a536682.

Files selected for processing: 3
  • libs/sdk/src/common/metadata/http-route.metadata.ts
  • libs/sdk/src/http-route/__tests__/http-route.validation.spec.ts
  • libs/skills/catalog/frontmcp-development/references/create-http-route.md
Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 2e390cc and 24acbf1.

Files selected for processing: 64
  • apps/e2e/demo-e2e-bundle-size/e2e/browser-bundle-size.e2e.spec.ts
  • apps/e2e/demo-e2e-dts-consumer/e2e/dts-consumer.e2e.spec.ts
  • apps/e2e/demo-e2e-dts-consumer/fixture/http-routes-must-compile.ts
  • apps/e2e/demo-e2e-dts-consumer/fixture/tsconfig.json
  • apps/e2e/demo-e2e-web-host/e2e/web-host.e2e.spec.ts
  • apps/e2e/demo-e2e-web-host/jest.e2e.config.ts
  • apps/e2e/demo-e2e-web-host/project.json
  • apps/e2e/demo-e2e-web-host/src/apps/board/board.routes.ts
  • apps/e2e/demo-e2e-web-host/src/apps/board/board.store.ts
  • apps/e2e/demo-e2e-web-host/src/apps/board/board.tools.ts
  • apps/e2e/demo-e2e-web-host/src/apps/board/index.ts
  • apps/e2e/demo-e2e-web-host/src/apps/board/visit-counter.provider.ts
  • apps/e2e/demo-e2e-web-host/src/main.ts
  • apps/e2e/demo-e2e-web-host/tsconfig.app.json
  • apps/e2e/demo-e2e-web-host/tsconfig.e2e.json
  • apps/e2e/demo-e2e-web-host/tsconfig.json
  • apps/e2e/demo-e2e-web-host/webpack.config.js
  • libs/auth/src/index.ts
  • libs/auth/src/people/__tests__/loopback-token.auth.spec.ts
  • libs/auth/src/people/index.ts
  • libs/auth/src/people/loopback-token.auth.ts
  • libs/auth/src/people/people-auth.options.ts
  • libs/sdk/src/common/decorators/http-route.decorator.ts
  • libs/sdk/src/common/decorators/index.ts
  • libs/sdk/src/common/entries/scope.entry.ts
  • libs/sdk/src/common/interfaces/http-route.interface.ts
  • libs/sdk/src/common/interfaces/index.ts
  • libs/sdk/src/common/metadata/app.metadata.ts
  • libs/sdk/src/common/metadata/http-route.metadata.ts
  • libs/sdk/src/common/metadata/index.ts
  • libs/sdk/src/common/tokens/app.tokens.ts
  • libs/sdk/src/common/types/options/http/index.ts
  • libs/sdk/src/common/types/options/http/interfaces.ts
  • libs/sdk/src/common/types/options/http/schema.ts
  • libs/sdk/src/errors/http-route.errors.ts
  • libs/sdk/src/errors/index.ts
  • libs/sdk/src/hooks/__tests__/fixtures/typed-hook-ctx.fixture.ts
  • libs/sdk/src/http-route/__tests__/http-route.flow.spec.ts
  • libs/sdk/src/http-route/__tests__/http-route.validation.spec.ts
  • libs/sdk/src/http-route/__tests__/sse-route.stream.spec.ts
  • libs/sdk/src/http-route/flows/http.route.flow.ts
  • libs/sdk/src/http-route/http-route.table.ts
  • libs/sdk/src/http-route/people-sign-in.routes.ts
  • libs/sdk/src/http-route/sse-route.stream.ts
  • libs/sdk/src/index.ts
  • libs/sdk/src/scope/http-scope.helper.ts
  • libs/sdk/src/scope/scope.instance.ts
  • libs/sdk/src/server/__tests__/write-http-response.sse.spec.ts
  • libs/sdk/src/server/security/__tests__/same-origin.spec.ts
  • libs/sdk/src/server/security/same-origin.ts
  • libs/sdk/src/server/server.validation.ts
  • libs/sdk/src/transport/web-fetch-handler.ts
  • libs/skills/__tests__/skills-validation.spec.ts
  • libs/skills/catalog/frontmcp-config/references/configure-http.md
  • libs/skills/catalog/frontmcp-development/SKILL.md
  • libs/skills/catalog/frontmcp-development/references/create-http-route.md
  • libs/skills/catalog/frontmcp-development/references/decorators-guide.md
  • libs/skills/catalog/frontmcp-development/references/official-plugins.md
  • libs/skills/catalog/skills-manifest.json
  • plugins/plugin-dashboard/src/README.md
  • plugins/plugin-dashboard/src/__tests__/dashboard.app.spec.ts
  • plugins/plugin-dashboard/src/__tests__/dashboard.page-route.spec.ts
  • plugins/plugin-dashboard/src/app/dashboard-page.route.ts
  • plugins/plugin-dashboard/src/app/dashboard.app.ts

Included review availability: This review used your included allowance. Your plan provides up to 5 included reviews per hour; 0 remain after this review.

Walkthrough

This PR adds SDK HTTP routes with people authentication, SSE responses, and server integration. It migrates the dashboard page from middleware to a route and adds demo E2E coverage, declaration-consumer checks, and feature documentation.

Changes: HTTP Routes and People Authentication

Layer / File(s) Summary
Route and authentication contracts
libs/auth/src/people/*, libs/sdk/src/common/decorators/*, libs/sdk/src/common/interfaces/*, libs/sdk/src/common/metadata/*, libs/sdk/src/common/types/options/http/*, libs/sdk/src/errors/*
Adds route metadata, @HttpRoute, HttpRouteContext, app route registration, HTTP authentication options, and route-related errors.
Loopback-token authentication
libs/auth/src/people/*, libs/auth/src/index.ts
Adds token and code exchanges, expiring sessions, client lockouts, cookie construction, configuration validation, exports, and tests.
Route registration, execution, and SSE
libs/sdk/src/http-route/*
Adds route-table construction, built-in sign-in routes, request flow handling, SSE streams, and tests for route validation, authentication, responses, hooks, and stream lifecycle.
Server and fetch-handler integration
libs/sdk/src/scope/*, libs/sdk/src/transport/web-fetch-handler.ts, libs/sdk/src/server/*, libs/sdk/src/hooks/__tests__/*
Registers routes with scopes, checks route claims in fetch handling, supports SSE backpressure and closure, and adds same-origin and hook-type coverage.
Dashboard page route migration
plugins/plugin-dashboard/src/app/*, plugins/plugin-dashboard/src/__tests__/*, plugins/plugin-dashboard/src/README.md, libs/skills/catalog/frontmcp-development/references/official-plugins.md
Replaces dashboard page middleware with a route and updates dashboard tests and documentation.
Web-host demo and E2E coverage
apps/e2e/demo-e2e-web-host/*
Adds a board app and E2E tests for sign-in, access controls, same-origin mutations, SSE updates, and shared provider state.
SDK consumers and feature documentation
apps/e2e/demo-e2e-dts-consumer/*, apps/e2e/demo-e2e-bundle-size/*, libs/skills/*
Adds route declaration compilation coverage, updates browser bundle budgets and leak checks, and documents route setup, authentication, SSE, and flow behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant HttpRouteFlow
  participant HttpRouteTable
  participant LoopbackTokenAuth
  participant SseRouteStream
  participant ResourceStore
  Browser->>HttpRouteFlow: Send route request
  HttpRouteFlow->>HttpRouteTable: Match method and path
  HttpRouteFlow->>LoopbackTokenAuth: Verify session when required
  HttpRouteFlow->>SseRouteStream: Open SSE response
  SseRouteStream->>ResourceStore: Subscribe to route topics
  ResourceStore->>SseRouteStream: Notify topic update
  SseRouteStream->>Browser: Send encoded event
Loading

Suggested reviewers: alexmercerpo

Merge Risk: Low · previous review 24acb

The new route guide misstates path syntax and does not warn that route registration order matters when a parameter route overlaps an SSE path. Readers following it could register a route that never fires. The code change itself looks sound, so these are small documentation fixes that can be made before or shortly after merge.

Pre-merge checks: Passed 4 · Inconclusive 1

Failed Failed checks: 1 inconclusive

Check name Status Explanation Resolution
Docstring Coverage Inconclusive Docstring coverage is 30.95% which is insufficient. The required threshold is 65.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title names the main change: a web host with @HttpRoute pages and SSE routes through a hookable http:route flow, plus loopback-token sign-in. The changeset matches this, including the route classe…
Description check Passed The description follows the template's required sections: 'What and why', 'User-facing change' (with what changed, before/after, frontmcp.dev pages, and skills catalog entries), and 'Tests'. It is det…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title names the main change: a web host with @HttpRoute pages and SSE routes through a hookable http:route flow, plus loopback-token sign-in. The changeset matches this, including the route classes, the http:route flow, SSE streams, and the people auth in libs/auth. The title is long (124 characters), but it stays specific and on topic.

Full details: Description check

Explanation

The description follows the template's required sections: 'What and why', 'User-facing change' (with what changed, before/after, frontmcp.dev pages, and skills catalog entries), and 'Tests'. It is detailed and covers the changes, the threat model, known limits, and test results. The 'Tests' section is thorough. The 'User-facing change' section is filled in, and the template's guidance on this section applies because the PR changes public APIs and error messages.

Full details: Docstring Coverage

Explanation

Docstring coverage is 30.95% which is insufficient. The required threshold is 65.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 42 functions across 50 files. (14 skipped: 12 unsupported, 2 over the file limit.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor
Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Performance Test Results

Status: ✅ All tests passed

Summary

Project Tests Passed Warnings Failed Leaks
✅ demo-e2e-agents 4 4 0 0 0
✅ demo-e2e-cache 11 11 0 0 0
✅ demo-e2e-codecall 4 4 0 0 0
✅ demo-e2e-config 4 4 0 0 0
✅ demo-e2e-direct 3 3 0 0 0
✅ demo-e2e-elicitation 1 1 0 0 0
✅ demo-e2e-errors 4 4 0 0 0
✅ demo-e2e-hooks 3 3 0 0 0
✅ demo-e2e-multiapp 4 4 0 0 0
✅ demo-e2e-notifications 3 3 0 0 0
✅ demo-e2e-openapi 2 2 0 0 0
✅ demo-e2e-providers 4 4 0 0 0
✅ demo-e2e-public 4 4 0 0 0
✅ demo-e2e-redis 15 15 0 0 0
✅ demo-e2e-remember 4 4 0 0 0
✅ demo-e2e-remote 5 5 0 0 0
✅ demo-e2e-serverless 2 2 0 0 0
✅ demo-e2e-skills 15 15 0 0 0
✅ demo-e2e-standalone 2 2 0 0 0
✅ demo-e2e-transport-recreation 3 3 0 0 0
✅ demo-e2e-ui 4 4 0 0 0

Total: 101 tests across 21 projects

📊 View full report in workflow run


Generated at: 2026-11-02T22:13:39.089Z
Commit: 1d1c2fd6

…nto feat/m16-web-host

# Conflicts:
#	apps/e2e/demo-e2e-bundle-size/e2e/browser-bundle-size.e2e.spec.ts
…1019's new head

Measured against #1019's head, the web host grows create() + one tool and
create() + WebMcpPlugin + one tool by 608 bytes each; the budgets move up from
#1019's by exactly that.
@frontegg-david

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor
Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@frontegg-david

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor
Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Fix comments: Fix CodeRabbit comments on this PR

@frontegg-david
frontegg-david marked this pull request as draft November 2, 2026 21:59
… and document route order

The route table matches literal segments and :name parameters only, but a
path such as /files/*path or /board/:id(\d+) was accepted and then never
matched any request. Such a path is now refused where the route is declared,
with a message naming the syntax it takes.

The create-http-route guide lists the SSE route before the parameter route
it would otherwise be shadowed by, and says the first route that matches, in
the order routes lists them, answers.
@frontegg-david
frontegg-david marked this pull request as ready for review November 2, 2026 21:59
@frontegg-david

Copy link
Copy Markdown
Contributor Author

Local runs on a536682: SDK http-route suites 94/94, plugin-dashboard 223/223, skills 249/249; e2e web-host 7, dashboard 12, security 28; bundle-size 12, unchanged (1,332,923 / 1,340,102).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants