src/main.ts
TLS configuration options for HTTPS server.
Properties |
|
| ca |
ca:
|
Type : Buffer
|
| Optional |
| cert |
cert:
|
Type : Buffer
|
| key |
key:
|
Type : Buffer
|
| passphrase |
passphrase:
|
Type : string
|
| Optional |
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { RequestMethod, ValidationPipe } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { NestFactory } from "@nestjs/core";
import { NestExpressApplication } from "@nestjs/platform-express";
import {
DocumentBuilder,
type OpenAPIObject,
SwaggerModule,
} from "@nestjs/swagger";
import { Logger } from "nestjs-pino";
import { cleanupOpenApiDoc } from "nestjs-zod";
import { AllExceptionsFilter } from "./all-exceptions.filter";
import { AppModule } from "./app.module";
import { ValidationErrorFilter } from "./shared/common/filters/validation-error.filter";
import otelSDK from "./tracing";
/**
* Protocol routes excluded from the `/api` global prefix.
* These are wallet-facing and infrastructure endpoints that must remain
* at the root path for protocol compliance and discoverability.
*/
const PROTOCOL_ROUTE_EXCLUSIONS: { path: string; method: RequestMethod }[] = [
// Infrastructure
{ path: "/", method: RequestMethod.GET },
{ path: "health", method: RequestMethod.ALL },
// OAuth2 & Discovery
{ path: "oauth2/{*path}", method: RequestMethod.ALL },
{ path: ".well-known/{*path}", method: RequestMethod.ALL },
// OID4VCI Protocol
{ path: "issuers/:tenantId/vci/{*path}", method: RequestMethod.ALL },
{ path: "issuers/:tenantId/authorize", method: RequestMethod.ALL },
{ path: "issuers/:tenantId/authorize/{*path}", method: RequestMethod.ALL },
{
path: "issuers/:tenantId/credentials-metadata/{*path}",
method: RequestMethod.ALL,
},
{ path: "issuers/:tenantId/chained-as/{*path}", method: RequestMethod.ALL },
// OID4VP Protocol
{ path: "presentations/:sessionId/oid4vp", method: RequestMethod.ALL },
{
path: "presentations/:sessionId/oid4vp/{*path}",
method: RequestMethod.ALL,
},
// Public Status & Trust Lists
{
path: "issuers/:tenantId/status-management/{*path}",
method: RequestMethod.ALL,
},
{ path: "issuers/:tenantId/trust-list/{*path}", method: RequestMethod.ALL },
// Public Storage (credential images, logos)
{ path: "storage/:key", method: RequestMethod.GET },
];
/**
* Filter an OpenAPI document to only include paths matching (or not matching)
* a given prefix. Also prunes the tag list to only include used tags.
*/
function filterOpenApiPaths(
document: OpenAPIObject,
predicate: (path: string) => boolean,
): OpenAPIObject {
const filteredPaths: OpenAPIObject["paths"] = {};
const usedTags = new Set<string>();
for (const [path, pathItem] of Object.entries(document.paths)) {
if (predicate(path)) {
filteredPaths[path] = pathItem;
for (const operation of Object.values(
pathItem as Record<string, any>,
)) {
if (operation?.tags) {
for (const tag of operation.tags) {
usedTags.add(tag);
}
}
}
}
}
return {
...document,
paths: filteredPaths,
tags: document.tags?.filter((tag: { name: string }) =>
usedTags.has(tag.name),
),
};
}
/**
* TLS configuration options for HTTPS server.
*/
interface TlsOptions {
cert: Buffer;
key: Buffer;
ca?: Buffer;
passphrase?: string;
}
/**
* Load TLS options from certificate and key files.
* Returns undefined if TLS is not enabled or files are not found.
*/
function loadTlsOptions(): TlsOptions | undefined {
const tlsEnabled = process.env.TLS_ENABLED?.toLowerCase() === "true";
if (!tlsEnabled) {
return undefined;
}
const certPath = process.env.TLS_CERT_PATH;
const keyPath = process.env.TLS_KEY_PATH;
const caPath = process.env.TLS_CA_PATH;
if (!certPath || !keyPath) {
console.warn(
"⚠️ TLS_ENABLED is true but TLS_CERT_PATH or TLS_KEY_PATH is not set. Falling back to HTTP.",
);
return undefined;
}
if (!existsSync(certPath)) {
console.warn(
`⚠️ TLS certificate file not found: ${certPath}. Falling back to HTTP.`,
);
return undefined;
}
if (!existsSync(keyPath)) {
console.warn(
`⚠️ TLS key file not found: ${keyPath}. Falling back to HTTP.`,
);
return undefined;
}
const options: TlsOptions = {
cert: readFileSync(certPath),
key: readFileSync(keyPath),
};
// Optional: Load CA certificate chain for client verification
if (caPath && existsSync(caPath)) {
options.ca = readFileSync(caPath);
}
// Optional: Passphrase for encrypted key files
const passphrase = process.env.TLS_KEY_PASSPHRASE;
if (passphrase) {
options.passphrase = passphrase;
}
return options;
}
/**
* Bootstrap function to initialize the NestJS application.
*/
async function bootstrap() {
// Start OpenTelemetry SDK before NestJS initializes —
// instrumentations must be registered before any framework code runs.
await otelSDK.start();
// Load TLS options if configured
const tlsOptions = loadTlsOptions();
const isTlsEnabled = tlsOptions !== undefined;
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
bufferLogs: true,
snapshot: true,
httpsOptions: tlsOptions,
});
// Use Pino logger for all NestJS logging (including built-in Logger instances)
// This ensures LOG_LEVEL env var is respected across all services
app.useLogger(app.get(Logger));
// Set explicit body size limits (security best practice)
app.useBodyParser("json", { limit: "10mb" });
app.enableCors();
// Global route prefix: all management endpoints under /api/,
// protocol endpoints (wallet-facing) stay at root for compliance
app.setGlobalPrefix("api", { exclude: PROTOCOL_ROUTE_EXCLUSIONS });
// Global exception filter for ValidationError
app.useGlobalFilters(
new ValidationErrorFilter(),
new AllExceptionsFilter(),
);
app.useGlobalPipes(
new ValidationPipe({
transform: true, // required for discriminator instantiation
whitelist: true,
forbidUnknownValues: false, // avoid false positives on plain objects
forbidNonWhitelisted: false,
stopAtFirstError: false,
validateCustomDecorators: true,
}),
);
const configService = app.get(ConfigService);
const publicUrl = configService.getOrThrow<string>("PUBLIC_URL");
const useExternalOIDC = configService.get<string>("OIDC");
// ── Management API OpenAPI config ────────────────────────────────
const managementConfigBuilder = new DocumentBuilder()
.setTitle("EUDIPLO Management API")
.setDescription(
"API for managing credentials, sessions, keys, and configurations. " +
"All endpoints require OAuth2 authentication.",
)
.setExternalDoc(
"Documentation",
"https://openwallet-foundation-labs.github.io/eudiplo/latest/",
)
.setOpenAPIVersion("3.1.0")
.setVersion(process.env.VERSION ?? "main");
if (useExternalOIDC) {
const oidcIssuerUrl = configService.get<string>(
"OIDC_INTERNAL_ISSUER_URL",
);
if (oidcIssuerUrl) {
managementConfigBuilder.addOAuth2(
{
type: "openIdConnect",
openIdConnectUrl: `${oidcIssuerUrl}/.well-known/openid-configuration`,
},
"oauth2",
);
}
} else if (publicUrl) {
managementConfigBuilder.addOAuth2(
{
type: "oauth2",
flows: {
clientCredentials: {
tokenUrl: `${publicUrl}/oauth2/token`,
scopes: {},
},
},
},
"oauth2",
);
}
const managementDocConfig = managementConfigBuilder.build();
// ── Protocol API OpenAPI config ──────────────────────────────────
const protocolDocConfig = new DocumentBuilder()
.setTitle("EUDIPLO Protocol API")
.setDescription(
"Wallet-facing protocol endpoints for OID4VCI, OID4VP, and related standards. " +
"These endpoints are public and secured at the protocol level (DPoP, Wallet Attestation, etc.).",
)
.setExternalDoc(
"Documentation",
"https://openwallet-foundation-labs.github.io/eudiplo/latest/",
)
.setOpenAPIVersion("3.1.0")
.setVersion(process.env.VERSION ?? "main")
.build();
// ── Document factories ───────────────────────────────────────────
const fullDocFactory = () =>
cleanupOpenApiDoc(
SwaggerModule.createDocument(app, managementDocConfig),
);
const managementDocFactory = () =>
filterOpenApiPaths(fullDocFactory(), (path) =>
path.startsWith("/api/"),
);
const protocolDocFactory = () =>
filterOpenApiPaths(
cleanupOpenApiDoc(
SwaggerModule.createDocument(app, protocolDocConfig),
),
(path) => !path.startsWith("/api/"),
);
if (process.env.DOC_GENERATE) {
writeFileSync(
"swagger-management.json",
JSON.stringify(managementDocFactory(), null, 2),
);
writeFileSync(
"swagger-protocol.json",
JSON.stringify(protocolDocFactory(), null, 2),
);
process.exit();
} else {
const sharedSwaggerOptions = {
swaggerOptions: {
persistAuthorization: true,
displayRequestDuration: true,
filter: true,
showExtensions: true,
showCommonExtensions: true,
tryItOutEnabled: true,
deepLinking: true,
displayOperationId: false,
defaultModelsExpandDepth: 1,
defaultModelExpandDepth: 1,
docExpansion: "list",
operationsSorter: "alpha",
tagsSorter: "alpha",
},
};
// Cache-control headers for Swagger UI assets
for (const swaggerPath of ["/api/docs", "/docs"]) {
app.use(swaggerPath, (_req, res, next) => {
res.setHeader(
"Cache-Control",
"no-cache, no-store, must-revalidate",
);
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0");
next();
});
}
SwaggerModule.setup("/api/docs", app, managementDocFactory, {
...sharedSwaggerOptions,
customSiteTitle: "EUDIPLO Management API",
});
SwaggerModule.setup("/docs", app, protocolDocFactory, {
...sharedSwaggerOptions,
customSiteTitle: "EUDIPLO Protocol API",
});
const logger =
app.getHttpAdapter().getInstance().locals?.logger || console;
const oidc = configService.get<string>("OIDC");
await app.listen(process.env.PORT ?? 3000).then(() => {
const port = process.env.PORT ?? 3000;
const publicUrl = configService.get<string>("PUBLIC_URL");
const version = process.env.VERSION ?? "main";
const nodeEnv = process.env.NODE_ENV ?? "development";
const protocol = isTlsEnabled ? "https" : "http";
const baseUrl = publicUrl || `${protocol}://localhost:${port}`;
logger.log("");
logger.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
logger.log("🚀 EUDIPLO Service Started Successfully");
logger.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
logger.log(`📦 Version: ${version}`);
logger.log(`🌍 Environment: ${nodeEnv}`);
logger.log(`🔌 Port: ${port}`);
logger.log(
`🔒 TLS: ${isTlsEnabled ? "Enabled" : "Disabled (use reverse proxy for HTTPS)"}`,
);
logger.log(`🌐 Public URL: ${publicUrl || "Not configured"}`);
logger.log("");
logger.log("📚 API Documentation:");
logger.log(` → Management: ${baseUrl}/api/docs`);
logger.log(` → Protocol: ${baseUrl}/docs`);
logger.log(
` → Full Docs: https://openwallet-foundation-labs.github.io/eudiplo/latest/`,
);
logger.log("");
logger.log("🏥 Health Check:");
logger.log(` → Endpoint: ${baseUrl}/health`);
logger.log("");
logger.log("🔐 Authentication:");
if (oidc) {
logger.log(` → Mode: External OIDC`);
logger.log(` → Provider: ${oidc}`);
} else {
logger.log(
` → Mode: Integrated OAuth2 (Client Credentials)`,
);
logger.log(` → Token URL: ${publicUrl}/oauth2/token`);
}
});
}
}
void bootstrap();