33/**
44 * [#23050 slice 7] The API-description routes of `RestServer` — the discovery
55 * document (`GET {basePath}` and `GET {basePath}/discovery`) and the OpenAPI
6- * pair (`GET {basePath}/openapi.json` and its `GET {basePath}/docs` viewer) —
7- * moved out of `rest-server.ts` whole. Both registrars run once per mounted
8- * base, so an environment-scoped deployment mounts each family twice.
6+ * document (`GET {basePath}/openapi.json`) — moved out of `rest-server.ts`
7+ * whole. Both registrars run once per mounted base, so an environment-scoped
8+ * deployment mounts each family twice.
9+ *
10+ * [#25104] The OpenAPI document's HTML viewer page, `GET {basePath}/docs`,
11+ * moved here with it and has since been RETIRED on both route shapes: nothing
12+ * read it, and the API origin serves no HTML viewer. A request to that path
13+ * meets the host's ordinary not-found answer; the machine-readable document
14+ * stays. `rest-docs-viewer-retired.pin.test.ts` pins both halves.
915 *
1016 * A MOVE, not a redesign. The two functions below are the bodies of
1117 * `RestServer#registerDiscoveryEndpoints` and
1218 * `RestServer#registerOpenApiEndpoints` as they stood, each re-indented one
13- * level — except the viewer page's HTML, a template literal whose lines are
14- * runtime text and sit at column 0, kept byte for byte — the same routes,
15- * registered in the same order, by the same handlers, answering the same
16- * bodies. Every path expression (`basePath`, `realBase`, `unscopedBase`) is
17- * as written. The imports only these bodies read moved with them; the two
19+ * level — the same routes, registered in the same order, by the same
20+ * handlers, answering the same bodies. Every path expression (`basePath`,
21+ * `realBase`, `unscopedBase`) is as written. The imports only these bodies
22+ * read moved with them; the two
1823 * registrars share ONE module because they are the one API-description family
1924 * (`/discovery` advertises the surfaces `/openapi.json` describes), and they
2025 * still share no code, exactly as before.
6772// half is the class's `probeMcpServeable`, reached through the host.
6873import { isMcpServerEnabled } from '@objectstack/types' ;
6974import { handleRouteError , sendThrownError } from './error-response.js' ;
70- // [#25077] The docs viewer page escapes the value it places into its attribute.
71- import { escapeHtml } from './html-escape.js' ;
7275import { logError } from './log.js' ;
7376// [#5040 E6] The `api`-metadata endpoints the OpenAPI document folds in.
7477import { enrichOpenApiWithEndpoints } from './openapi-endpoints.js' ;
@@ -447,26 +450,23 @@ export function registerDiscoveryEndpoints(this: DiscoveryRouteHost, basePath: s
447450}
448451
449452/**
450- * Register the OpenAPI document and its viewer — `RestServer#registerOpenApiEndpoints`'
453+ * Register the OpenAPI document — `RestServer#registerOpenApiEndpoints`'
451454 * body, called by that method with the server as `this`. The method's own
452- * docblock, which the published declaration carries, names the two routes and
455+ * docblock, which the published declaration carries, names the route and
453456 * the request-time enrichment of the document.
454457 */
455458export function registerOpenApiEndpoints ( this : OpenApiRouteHost , basePath : string ) : void {
456459 const isScoped = basePath . includes ( '/environments/:environmentId' ) ;
457460
458- // [#22430] Both API-description endpoints — the document and its
459- // viewer — refuse an anonymous caller with the shared anonymous-deny
460- // 401, the same body the `/data` and `/meta` routes answer (ADR-0056 D2
461- // is default-deny, and no ADR-0138 D2 door class names these two). The
462- // refusal is each handler's FIRST act: before the bundled artifact is
463- // loaded and before any protocol is resolved, so a refused request
464- // costs no work and its answer does not even say whether a document is
465- // bundled here. A signed-in caller is served exactly as before, under
466- // the same ADR-0069 auth-policy gate every other protected route
467- // applies. The viewer page fetches the document from the browser, and
468- // that request carries the browser's session, so a signed-in browser is
469- // served on both endpoints.
461+ // [#22430] The API-description document refuses an anonymous caller
462+ // with the shared anonymous-deny 401, the same body the `/data` and
463+ // `/meta` routes answer (ADR-0056 D2 is default-deny, and no ADR-0138
464+ // D2 door class names it). The refusal is the handler's FIRST act:
465+ // before the bundled artifact is loaded and before any protocol is
466+ // resolved, so a refused request costs no work and its answer does not
467+ // even say whether a document is bundled here. A signed-in caller is
468+ // served exactly as before, under the same ADR-0069 auth-policy gate
469+ // every other protected route applies.
470470
471471 const openApiHandler = async ( req : any , res : any ) => {
472472 try {
@@ -487,8 +487,9 @@ export function registerOpenApiEndpoints(this: OpenApiRouteHost, basePath: strin
487487 // expanded paths) don't bleed into the cached base spec.
488488 let enriched : any = { ...spec , servers : [ ...( spec . servers ?? [ ] ) ] } ;
489489
490- // 1) Override servers[0] with the actual request origin so
491- // "Try it" works straight from the docs viewer.
490+ // 1) Override servers[0] with the actual request origin so a
491+ // client built from this document calls the server that
492+ // served it.
492493 const host = req . headers ?. host ?? req . headers ?. [ 'host' ] ;
493494 const proto = ( req . headers ?. [ 'x-forwarded-proto' ] as string )
494495 || ( req . protocol as string )
@@ -672,56 +673,4 @@ export function registerOpenApiEndpoints(this: OpenApiRouteHost, basePath: strin
672673 tags : [ 'openapi' ] ,
673674 } ,
674675 } ) ;
675-
676- // Scalar HTML viewer — single inline page that loads the spec from
677- // the sibling /openapi.json endpoint. No build-time bundling, no
678- // server-side render cost.
679- this . routeManager . register ( {
680- method : 'GET' ,
681- path : `${ basePath } /docs` ,
682- handler : async ( req : any , res : any ) => {
683- try {
684- const environmentId = isScoped ? req . params ?. environmentId : undefined ;
685- const context = await this . resolveExecCtx ( environmentId , req ) ;
686- if ( this . enforceAuth ( req , res , context ) ) return ;
687-
688- // Resolve the openapi.json URL relative to the current
689- // request so the docs page works for any host / scoped
690- // base path (e.g. /api/v1 vs /api/v1/environments/abc).
691- const reqPath : string = req . path || req . url || `${ basePath } /docs` ;
692- // Strip the trailing /docs to get the API base.
693- const apiBase = reqPath . replace ( / \/ d o c s \/ ? $ / , '' ) ;
694- const specUrl = `${ apiBase } /openapi.json` ;
695- // [#25077] Escaped for the attribute it lands in (ruling 3C on
696- // #25040: every outlet that renders a value into HTML escapes it
697- // by context). A path without HTML-special characters renders
698- // byte for byte as before. `html-outlets.pin.test.ts` pins both.
699- const html = `<!doctype html>
700- <html>
701- <head>
702- <meta charset="utf-8" />
703- <meta name="viewport" content="width=device-width, initial-scale=1" />
704- <title>ObjectStack API Docs</title>
705- </head>
706- <body>
707- <script id="api-reference" data-url="${ escapeHtml ( specUrl ) } "></script>
708- <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
709- </body>
710- </html>` ;
711- if ( res . setHeader ) res . setHeader ( 'content-type' , 'text/html; charset=utf-8' ) ;
712- if ( res . send ) res . send ( html ) ;
713- else if ( res . body ) res . body = html ;
714- else res . json ?.( html ) ;
715- } catch ( error : any ) {
716- // Only the identity read above can throw here (a
717- // permission-store outage), which keeps its declared answer.
718- logError ( '[REST] docs viewer error:' , error ) ;
719- sendThrownError ( res , error ) ;
720- }
721- } ,
722- metadata : {
723- summary : 'Interactive API docs (Scalar viewer)' ,
724- tags : [ 'openapi' ] ,
725- } ,
726- } ) ;
727676}
0 commit comments