UNPKG

@sphereon/ssi-sdk.siopv2-oid4vp-rp-rest-api

Version:

173 lines (150 loc) • 6.76 kB
import { agentContext } from '@sphereon/ssi-sdk.core' import { copyGlobalAuthToEndpoints, ExpressSupport } from '@sphereon/ssi-express-support' import { ISIOPv2RP } from '@sphereon/ssi-sdk.siopv2-oid4vp-rp-auth' import { TAgent } from '@veramo/core' import express, { Express, Request, Response, Router } from 'express' import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) import { getAuthRequestSIOPv2Endpoint, verifyAuthResponseSIOPv2Endpoint } from './siop-api-functions' import { IRequiredPlugins, ISIOPv2RPRestAPIOpts } from './types' import { authStatusUniversalOID4VPEndpoint, createAuthRequestUniversalOID4VPEndpoint, getDefinitionsEndpoint, removeAuthRequestStateUniversalOID4VPEndpoint, } from './universal-oid4vp-api-functions' import swaggerUi from 'swagger-ui-express' export class SIOPv2RPApiServer { private readonly _express: Express private readonly _router: Router private readonly _agent: TAgent<ISIOPv2RP> private readonly _opts?: ISIOPv2RPRestAPIOpts private readonly _basePath: string private readonly OID4VP_OPENAPI_FILE = path.join(__dirname, '..', 'oid4vp-openapi.yml') private readonly OID4VP_SWAGGER_URL = 'https://api.swaggerhub.com/apis/SphereonInt/OID4VP/0.1.0' constructor(args: { agent: TAgent<IRequiredPlugins>; expressSupport: ExpressSupport; opts?: ISIOPv2RPRestAPIOpts }) { const { agent, opts } = args this._agent = agent copyGlobalAuthToEndpoints({ opts, keys: ['webappCreateAuthRequest', 'webappAuthStatus', 'webappDeleteAuthRequest', 'webappGetDefinitions'] }) if (opts?.endpointOpts?.globalAuth?.secureSiopEndpoints) { copyGlobalAuthToEndpoints({ opts, keys: ['siopGetAuthRequest', 'siopVerifyAuthResponse'] }) } this._opts = opts this._express = args.expressSupport.express this._router = express.Router() const context = agentContext(agent) const features = opts?.enableFeatures ?? ['rp-status', 'siop'] console.log(`SIOPv2 API enabled, with features: ${JSON.stringify(features)}}`) // Webapp endpoints if (features.includes('rp-status')) { createAuthRequestUniversalOID4VPEndpoint(this._router, context, opts?.endpointOpts?.webappCreateAuthRequest) authStatusUniversalOID4VPEndpoint(this._router, context, opts?.endpointOpts?.webappAuthStatus) removeAuthRequestStateUniversalOID4VPEndpoint(this._router, context, opts?.endpointOpts?.webappDeleteAuthRequest) getDefinitionsEndpoint(this._router, context, opts?.endpointOpts?.webappGetDefinitions) } // SIOPv2 endpoints if (features.includes('siop')) { getAuthRequestSIOPv2Endpoint(this._router, context, opts?.endpointOpts?.siopGetAuthRequest) verifyAuthResponseSIOPv2Endpoint(this._router, context, opts?.endpointOpts?.siopVerifyAuthResponse) } this._basePath = opts?.endpointOpts?.basePath ?? '' this._express.use(this._basePath, this.router) this._express.set('trust proxy', opts?.endpointOpts?.trustProxy ?? true) this.setupSwaggerUi() } /** * Sets up Swagger UI for API documentation. * Spec source priority: * 1. OID4VP_OPENAPI_SPEC env var (URL or file path) * 2. Bundled universal-OID4VP-openapi.yaml file * 3. Fallback to SwaggerHub URL */ private setupSwaggerUi() { const envSpec = process.env.OID4VP_OPENAPI_SPEC const apiDocsPath = '/api-docs' const specPath = '/api-docs/spec.yaml' const fullApiDocsPath = `${this._basePath}${apiDocsPath}` const fullSpecPath = `${this._basePath}${specPath}` // Spec cache shared across all modes let cachedSpec: any = null // Determine spec source: env var > bundled file > SwaggerHub URL const isRemoteUrl = envSpec?.startsWith('http://') || envSpec?.startsWith('https://') const isEnvLocalFile = envSpec && !isRemoteUrl // Resolve relative paths from package root const resolvedEnvFile = isEnvLocalFile && !path.isAbsolute(envSpec) ? path.join(__dirname, '..', envSpec) : envSpec const hasBundledFile = fs.existsSync(this.OID4VP_OPENAPI_FILE) const localFile = isEnvLocalFile ? resolvedEnvFile : hasBundledFile ? this.OID4VP_OPENAPI_FILE : undefined const isLocalFile = !!localFile if (isEnvLocalFile && !fs.existsSync(resolvedEnvFile!)) { console.log(`[OID4VP] OpenAPI spec file not found at ${resolvedEnvFile}. Swagger UI disabled.`) return } const specSource = isRemoteUrl ? envSpec : isLocalFile ? localFile : this.OID4VP_SWAGGER_URL console.log(`[OID4VP] API docs: ${fullApiDocsPath} (spec: ${specSource})`) // Unified spec fetcher const getSpec = async (req?: Request): Promise<any> => { if (cachedSpec) { return cachedSpec } if (isLocalFile && localFile) { const content = fs.readFileSync(localFile, 'utf-8') try { cachedSpec = JSON.parse(content) } catch { // YAML - return as string, swagger-ui will parse it cachedSpec = content } } else { const url = isRemoteUrl ? envSpec! : this.OID4VP_SWAGGER_URL const response = await fetch(url) if (!response.ok) { throw new Error(`HTTP ${response.status}`) } const text = await response.text() try { cachedSpec = JSON.parse(text) } catch { cachedSpec = text } } // Set server URL if spec is JSON object if (typeof cachedSpec === 'object' && cachedSpec !== null && req) { cachedSpec.servers = [{ url: `${req.protocol}://${req.get('host')}${this._basePath}`, description: 'This server' }] } return cachedSpec } // Pre-fetch spec in background getSpec().catch((err) => console.log(`[OID4VP] Spec pre-fetch failed: ${err.message}`)) // Serve spec at dedicated endpoint this._router.get(specPath, async (req: Request, res: Response) => { try { const spec = await getSpec(req) if (typeof spec === 'string') { res.type('text/yaml').send(spec) } else { res.json(spec) } } catch (err: any) { console.error(`[OID4VP] Spec fetch error: ${err.message}`) res.status(502).json({ error: 'Failed to load OpenAPI spec', details: err.message }) } }) // Swagger UI this._router.use(apiDocsPath, swaggerUi.serve, swaggerUi.setup(undefined, { swaggerOptions: { url: fullSpecPath } })) } get express(): Express { return this._express } get router(): Router { return this._router } get agent(): TAgent<ISIOPv2RP> { return this._agent } get opts(): ISIOPv2RPRestAPIOpts | undefined { return this._opts } }