@@ -4,7 +4,6 @@ import { ISIOPv2RP } from '@sphereon/ssi-sdk.siopv2-oid4vp-rp-auth'
44import { TAgent } from '@veramo/core'
55import express , { Express , Request , Response , Router } from 'express'
66import fs from 'fs'
7- import path from 'path'
87import { getAuthRequestSIOPv2Endpoint , verifyAuthResponseSIOPv2Endpoint } from './siop-api-functions'
98import { IRequiredPlugins , ISIOPv2RPRestAPIOpts } from './types'
109import {
@@ -58,152 +57,122 @@ export class SIOPv2RPApiServer {
5857 this . setupSwaggerUi ( )
5958 }
6059
60+ /**
61+ * Sets up Swagger UI for API documentation.
62+ * Supports three modes via OID4VP_OPENAPI_SPEC environment variable:
63+ * - Remote URL: Fetches spec from URL and serves via proxy endpoint
64+ * - Local file: Serves spec from filesystem
65+ * - Default: Uses built-in SwaggerHub spec
66+ */
6167 private setupSwaggerUi ( ) {
6268 const openApiSpec = process . env . OID4VP_OPENAPI_SPEC
63- const isUrl = openApiSpec ?. startsWith ( 'http://' ) || openApiSpec ?. startsWith ( 'https://' )
69+ const apiDocsPath = '/api-docs'
70+ const specPath = '/api-docs/spec.yaml'
71+ const fullApiDocsPath = `${ this . _basePath } ${ apiDocsPath } `
72+ const fullSpecPath = `${ this . _basePath } ${ specPath } `
6473
65- if ( openApiSpec && isUrl ) {
66- const apiDocs = `${ this . _basePath } /api-docs`
67- console . log ( `[OID4VP] API docs available at ${ apiDocs } (using remote spec: ${ openApiSpec } )` )
74+ // Spec cache shared across all modes
75+ let cachedSpec : any = null
6876
69- let cachedSpec : string | null = null
70- let fetchPromise : Promise < string > | null = null
77+ // Determine spec source
78+ const isRemoteUrl = openApiSpec ?. startsWith ( 'http://' ) || openApiSpec ?. startsWith ( 'https://' )
79+ const isLocalFile = openApiSpec && ! isRemoteUrl
7180
72- // Helper function to fetch and cache the spec
73- const fetchSpec = async ( ) : Promise < string > => {
74- if ( cachedSpec ) {
75- return cachedSpec
76- }
81+ if ( isLocalFile && ! fs . existsSync ( openApiSpec ) ) {
82+ console . log ( `[OID4VP] OpenAPI spec file not found at ${ openApiSpec } . Swagger UI disabled.` )
83+ return
84+ }
7785
78- if ( fetchPromise ) {
79- return fetchPromise
80- }
86+ const specSource = isRemoteUrl ? openApiSpec : isLocalFile ? `file://${ openApiSpec } ` : this . OID4VP_SWAGGER_URL
87+ console . log ( `[OID4VP] API docs: ${ fullApiDocsPath } (spec: ${ specSource } )` )
8188
82- fetchPromise = fetch ( openApiSpec )
83- . then ( ( res ) => {
84- if ( ! res . ok ) {
85- throw new Error ( `Failed to fetch OpenAPI spec: HTTP ${ res . status } ` )
86- }
87- return res . text ( )
88- } )
89- . then ( ( text ) => {
90- cachedSpec = text
91- fetchPromise = null
92- return text
93- } )
94- . catch ( ( err ) => {
95- fetchPromise = null
96- throw err
97- } )
98-
99- return fetchPromise
89+ // Unified spec fetcher
90+ const getSpec = async ( req ?: Request ) : Promise < any > => {
91+ if ( cachedSpec ) {
92+ return cachedSpec
10093 }
10194
102- // Start fetching in the background for faster first load
103- fetchSpec ( ) . catch ( ( err ) => {
104- console . log ( `[OID4VP] Failed to pre-fetch remote OpenAPI spec: ${ err } ` )
105- } )
106-
107- // Serve the YAML/JSON at a proxied endpoint with on-demand fetching
108- this . _router . get ( '/api-docs/openapi.yaml' , async ( _req : Request , res : Response ) => {
95+ if ( isLocalFile ) {
96+ const content = fs . readFileSync ( openApiSpec , 'utf-8' )
10997 try {
110- const spec = await fetchSpec ( )
111- res . setHeader ( 'Content-Type' , 'text/yaml' )
112- res . send ( spec )
113- } catch ( err ) {
114- console . error ( `[OID4VP] Error serving OpenAPI spec: ${ err } ` )
115- res . status ( 502 ) . send ( `Failed to fetch OpenAPI spec: ${ err } ` )
98+ cachedSpec = JSON . parse ( content )
99+ } catch {
100+ // YAML - return as string, swagger-ui will parse it
101+ cachedSpec = content
116102 }
117- } )
118-
119- // Set up Swagger UI with the proxied endpoint
120- this . _router . use (
121- '/api-docs' ,
122- swaggerUi . serve ,
123- swaggerUi . setup ( undefined , {
124- swaggerOptions : {
125- url : `${ this . _basePath } /api-docs/openapi.yaml` ,
126- } ,
127- } ) ,
128- )
129- } else if ( openApiSpec ) {
130- if ( ! fs . existsSync ( openApiSpec ) ) {
131- console . log ( `[OID4VP] OpenAPI spec file not found at ${ openApiSpec } . Will not host api-docs on this instance` )
132- return
133- }
134- const apiDocs = `${ this . _basePath } /api-docs`
135- const specFileName = path . basename ( openApiSpec )
136- console . log ( `[OID4VP] API docs available at ${ apiDocs } (using local spec: ${ openApiSpec } )` )
137-
138- this . _router . get ( `/api-docs/${ specFileName } ` , ( _req : Request , res : Response ) => {
139- res . sendFile ( openApiSpec )
140- } )
141-
142- this . _router . use (
143- '/api-docs' ,
144- swaggerUi . serve ,
145- swaggerUi . setup ( undefined , {
146- swaggerOptions : {
147- url : `${ this . _basePath } /api-docs/${ specFileName } ` ,
148- } ,
149- } ) ,
150- )
151- } else {
152- const apiDocs = `${ this . _basePath } /api-docs`
153- console . log ( `[OID4VP] API docs available at ${ apiDocs } ` )
154-
155- let cachedSwagger : any = null
156- let fetchPromise : Promise < any > | null = null
157-
158- // Helper function to fetch and cache the swagger doc
159- const fetchSwagger = async ( ) : Promise < any > => {
160- if ( cachedSwagger ) {
161- return cachedSwagger
103+ } else {
104+ const url = isRemoteUrl ? openApiSpec ! : this . OID4VP_SWAGGER_URL
105+ const response = await fetch ( url )
106+ if ( ! response . ok ) {
107+ throw new Error ( `HTTP ${ response . status } ` )
162108 }
163-
164- if ( fetchPromise ) {
165- return fetchPromise
109+ const text = await response . text ( )
110+ try {
111+ cachedSpec = JSON . parse ( text )
112+ } catch {
113+ cachedSpec = text
166114 }
115+ }
167116
168- fetchPromise = fetch ( this . OID4VP_SWAGGER_URL )
169- . then ( ( res ) => res . json ( ) )
170- . then ( ( swagger : any ) => {
171- cachedSwagger = swagger
172- fetchPromise = null
173- return swagger
174- } )
175- . catch ( ( err ) => {
176- fetchPromise = null
177- throw err
178- } )
179-
180- return fetchPromise
117+ // Set server URL if spec is JSON object
118+ if ( typeof cachedSpec === 'object' && cachedSpec !== null && req ) {
119+ cachedSpec . servers = [ { url : `${ req . protocol } ://${ req . get ( 'host' ) } ${ this . _basePath } ` , description : 'This server' } ]
181120 }
182121
183- // Start fetching in the background for faster first load
184- fetchSwagger ( ) . catch ( ( err ) => {
185- console . log ( `[OID4VP] Failed to pre-fetch swagger document: ${ err } ` )
186- } )
187-
188- // Set up Swagger UI with on-demand spec loading
189- this . _router . use (
190- '/api-docs' ,
191- async ( req : Request , res : Response , next : any ) => {
192- try {
193- const swagger = await fetchSwagger ( )
194- swagger . servers = [ { url : `${ req . protocol } ://${ req . get ( 'host' ) } ${ this . _basePath } ` , description : 'This server' } ]
195- // @ts -ignore
196- req . swaggerDoc = swagger
197- next ( )
198- } catch ( err ) {
199- console . error ( `[OID4VP] Error loading swagger document: ${ err } ` )
200- res . status ( 502 ) . send ( `Failed to load API documentation: ${ err } ` )
201- }
202- } ,
203- swaggerUi . serveFiles ( undefined as any , { } ) ,
204- swaggerUi . setup ( ) ,
205- )
122+ return cachedSpec
123+ }
124+
125+ // Pre-fetch spec in background
126+ getSpec ( ) . catch ( ( err ) => console . log ( `[OID4VP] Spec pre-fetch failed: ${ err . message } ` ) )
127+
128+ // Serve spec at dedicated endpoint
129+ this . _router . get ( specPath , async ( req : Request , res : Response ) => {
130+ try {
131+ const spec = await getSpec ( req )
132+ if ( typeof spec === 'string' ) {
133+ res . type ( 'text/yaml' ) . send ( spec )
134+ } else {
135+ res . json ( spec )
136+ }
137+ } catch ( err : any ) {
138+ console . error ( `[OID4VP] Spec fetch error: ${ err . message } ` )
139+ res . status ( 502 ) . json ( { error : 'Failed to load OpenAPI spec' , details : err . message } )
140+ }
141+ } )
142+
143+ // Swagger UI - custom index handler must come BEFORE static serve
144+ const serveSwaggerIndex = ( req : Request , res : Response , next : any ) : void => {
145+ // Only handle exact /api-docs or /api-docs/ requests
146+ if ( req . path === '/' || req . path === '' ) {
147+ const html = `<!DOCTYPE html>
148+ <html lang="en">
149+ <head>
150+ <meta charset="UTF-8">
151+ <title>OID4VP API</title>
152+ <link rel="stylesheet" type="text/css" href="${ fullApiDocsPath } /swagger-ui.css">
153+ </head>
154+ <body>
155+ <div id="swagger-ui"></div>
156+ <script src="${ fullApiDocsPath } /swagger-ui-bundle.js"></script>
157+ <script src="${ fullApiDocsPath } /swagger-ui-standalone-preset.js"></script>
158+ <script>
159+ window.onload = function() {
160+ SwaggerUIBundle({
161+ url: "${ fullSpecPath } ",
162+ dom_id: '#swagger-ui',
163+ presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
164+ layout: "StandaloneLayout"
165+ });
166+ };
167+ </script>
168+ </body>
169+ </html>`
170+ res . type ( 'html' ) . send ( html )
171+ return
172+ }
173+ next ( )
206174 }
175+ this . _router . use ( apiDocsPath , serveSwaggerIndex , swaggerUi . serve )
207176 }
208177 get express ( ) : Express {
209178 return this . _express
0 commit comments