Skip to content

Commit 8125f6e

Browse files
committed
chore: swagger docs fixes
1 parent d4bcd1b commit 8125f6e

2 files changed

Lines changed: 160 additions & 154 deletions

File tree

packages/oid4vci-issuer-rest-api/src/OID4VCIRestAPI.ts

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@ import {
1010
createVerifyAuthResponseCallback,
1111
} from '@sphereon/ssi-sdk.oid4vci-issuer'
1212
import express, { Express, Request, Response, Router } from 'express'
13+
import fs from 'fs'
14+
import path from 'path'
15+
import { fileURLToPath } from 'url'
1316
import { IRequiredContext } from './types'
1417
import swaggerUi from 'swagger-ui-express'
1518

19+
const __filename = fileURLToPath(import.meta.url)
20+
const __dirname = path.dirname(__filename)
21+
1622
export interface IOID4VCIRestAPIOpts extends IOID4VCIServerOpts {}
1723

1824
export class OID4VCIRestAPI {
@@ -97,7 +103,7 @@ export class OID4VCIRestAPI {
97103
return new OID4VCIRestAPI({ context, issuerInstanceArgs, expressSupport, opts, instance, issuer })
98104
}
99105

100-
private readonly OID4VCI_SWAGGER_URL = 'https://api.swaggerhub.com/apis/SphereonInt/OID4VCI/0.1.1'
106+
private readonly OID4VCI_OPENAPI_FILE = path.join(__dirname, '..', 'oid4vci-openapi.yml')
101107

102108
private constructor(args: {
103109
issuer: VcIssuer
@@ -131,30 +137,61 @@ export class OID4VCIRestAPI {
131137
}
132138

133139
private setupSwaggerUi() {
134-
fetch(this.OID4VCI_SWAGGER_URL)
135-
.then((res) => res.json())
136-
.then((swagger: any) => {
137-
const apiDocs = `/api-docs`
138-
console.log(`[OID4VCI] API docs available at ${this._baseUrl.toString()}${this._basePath}${apiDocs}`)
139-
swagger.servers = [{ url: this._baseUrl.toString(), description: 'This server' }]
140-
this.express.set('trust proxy', this.opts?.endpointOpts?.trustProxy ?? true)
141-
this._router.use(
142-
apiDocs,
143-
(req: Request, res: Response, next: any) => {
144-
// @ts-ignore
145-
req.swaggerDoc = swagger
146-
next()
147-
},
148-
swaggerUi.serveFiles(swagger, options),
149-
swaggerUi.setup(),
150-
)
151-
})
152-
.catch((err) => {
153-
console.log(`[OID4VCI] Unable to fetch swagger document: ${err}. Will not host api-docs on this instance`)
154-
})
155-
const options = {
156-
// customCss: '.swagger-ui .topbar { display: none }',
140+
const apiDocsPath = `/api-docs`
141+
const specPath = `/api-docs/spec.yaml`
142+
const fullApiDocsPath = `${this._basePath}${apiDocsPath}`
143+
const fullSpecPath = `${this._basePath}${specPath}`
144+
145+
console.log(`[OID4VCI] API docs available at ${this._baseUrl.origin}${fullApiDocsPath}`)
146+
147+
this.express.set('trust proxy', this.opts?.endpointOpts?.trustProxy ?? true)
148+
149+
// Serve spec from local file
150+
this._router.get(specPath, (req: Request, res: Response): void => {
151+
try {
152+
if (!fs.existsSync(this.OID4VCI_OPENAPI_FILE)) {
153+
res.status(404).json({ error: 'OpenAPI spec file not found' })
154+
return
155+
}
156+
res.type('text/yaml').sendFile(this.OID4VCI_OPENAPI_FILE)
157+
} catch (err: any) {
158+
console.error(`[OID4VCI] Spec read error: ${err.message}`)
159+
res.status(500).json({ error: 'Failed to load OpenAPI spec', details: err.message })
160+
}
161+
})
162+
163+
// Swagger UI - custom index handler must come BEFORE static serve
164+
const serveSwaggerIndex = (req: Request, res: Response, next: any): void => {
165+
if (req.path === '/' || req.path === '') {
166+
const html = `<!DOCTYPE html>
167+
<html lang="en">
168+
<head>
169+
<meta charset="UTF-8">
170+
<title>OID4VCI API</title>
171+
<link rel="stylesheet" type="text/css" href="${fullApiDocsPath}/swagger-ui.css">
172+
</head>
173+
<body>
174+
<div id="swagger-ui"></div>
175+
<script src="${fullApiDocsPath}/swagger-ui-bundle.js"></script>
176+
<script src="${fullApiDocsPath}/swagger-ui-standalone-preset.js"></script>
177+
<script>
178+
window.onload = function() {
179+
SwaggerUIBundle({
180+
url: "${fullSpecPath}",
181+
dom_id: '#swagger-ui',
182+
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
183+
layout: "StandaloneLayout"
184+
});
185+
};
186+
</script>
187+
</body>
188+
</html>`
189+
res.type('html').send(html)
190+
return
191+
}
192+
next()
157193
}
194+
this._router.use(apiDocsPath, serveSwaggerIndex, swaggerUi.serve)
158195
}
159196

160197
get express(): Express {

packages/siopv2-oid4vp-rp-rest-api/src/siopv2-rp-api-server.ts

Lines changed: 99 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { ISIOPv2RP } from '@sphereon/ssi-sdk.siopv2-oid4vp-rp-auth'
44
import { TAgent } from '@veramo/core'
55
import express, { Express, Request, Response, Router } from 'express'
66
import fs from 'fs'
7-
import path from 'path'
87
import { getAuthRequestSIOPv2Endpoint, verifyAuthResponseSIOPv2Endpoint } from './siop-api-functions'
98
import { IRequiredPlugins, ISIOPv2RPRestAPIOpts } from './types'
109
import {
@@ -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

Comments
 (0)